Files
Chris Telfer d3b8772286 wasm: Don't order small blocks and add bulk free
This patch removes ordered block storage in fixed-sized block freelists
in the OPA WASM memory allocator.  Variable-sized block allocation still
orders blocks so that free() can coalesce them back into larger sized
blocks.  This greatly reduces the runtime of opa_free() for fixed-size
blocks as it turns it from an O(N) operation to an O(1) operation.

This comes at the cost that reducing the heap_ptr implicitly on
opa_free() becomes impractical since reduction will stop at the first
fixed-sied block regardless of whether it is allocated or not. In
practice, what this means is that the allocator can never combine
fixed-size and variable-sized blocks.  However, it was rarely able to do
so previously: only when the two blocks happened to be free at the same
time and line up with the heap_ptr.

This patch also adds support for a new function called opa_free_bulk() that
enables releasing memory objects always in O(1) time per object and
O(N log N) worst case for releasing N objects.  The patch works by
freeing variable-sized objects (which would normally take O(N) time per
free) to a temporary holding list and setting a flag indicating that the
next variable-sized allocation needs to merge said holding list.

When releasing the holding list, the memory allocator first merge-sorts
in address-order the released blocks and then merges and coalesces them
into the variable-sized block list in address order.  This takes at most
O(max(M+N, N log N)) time where M is the number of blocks on the
variable freelist and N is the number of blocks bulk freed.

The patch also updates the __opa_value_free() function to take a new
parameter named 'bulk' which directs the function passes to its various
type-specific subroutines.  Every time one of the type-sepcific
subroutines goes to free an object it invokes either opa_free() or
opa_free_bulk() depending upon the 'bulk' parameter.  (This is
abstracted by a function __opa_free_maybe_bulk() in value.c)
Calls to opa_value_free() or opa_value_free_shallow(), will set the
the 'bulk' parameter to false preserving the existing behavior.
However, the opa_value_add_path() and opa_value_remove_path()
functions will invoke the function with 'bulk' set to true to ensure
that the cascaded free operations on objects each take only O(1) time.

Finally, the patch re-enables the RESTAuthzAllow100Paths benchmark.

Fixes: #5901
Signed-off-by: Chris Telfer <chris.telfer@sophos.com>
2023-05-23 12:29:20 -07:00

721 lines
20 KiB
C

#include <string.h>
#include "std.h"
#include "stdlib.h"
#define WASM_PAGE_SIZE (65536)
#define ARRAY_SIZE(ARRAY) (sizeof(ARRAY) / sizeof((ARRAY)[0]))
static unsigned int heap_ptr;
static unsigned int heap_top;
extern unsigned char __heap_base; // set by lld
static void *builtin_cache[8];
struct heap_block {
size_t size;
struct heap_block *prev; // unset if block allocated
struct heap_block *next; // unset if block allocated
unsigned char data[0];
};
// free blocks, ordered per their memory address.
struct heap_blocks {
bool fixed_size;
size_t size; // if fixed size, this indicates the block size.
// if not fixed, this indicates the minimum block size.
struct heap_block start;
struct heap_block end;
};
// all the free blocks: fixed size blocks of 4, 8, 16 and 64 bytes and then one free
// list for varying sized blocks of 128 bytes or more.
static struct heap_blocks heap_free[5] = {
{true, 4},
{true, 8},
{true, 16},
{true, 64},
{false, 128},
};
static struct heap_blocks heap_stash[5] = {
{true, 4},
{true, 8},
{true, 16},
{true, 64},
{false, 128},
};
/*
* Currently, there is one variable sized blocklist. If there were more,
* we'd need to track each one here in heap_bulk_blocks.
*/
#define VARIABLE_SIZED_BLOCK_IDX (ARRAY_SIZE(heap_free)-1)
static struct heap_blocks heap_bulk_blocks = { false, 128 };
static bool variable_block_update_required = false;
#ifdef DEBUG
#define HEAP_CHECK(blocks) heap_check(__FUNCTION__, blocks)
#else
#define HEAP_CHECK(blocks)
#endif
static void init_free()
{
for (int i = 0; i < ARRAY_SIZE(heap_free); i++) {
heap_free[i].start = (struct heap_block) { 0, NULL, &heap_free[i].end };
heap_free[i].end = (struct heap_block) { 0, &heap_free[i].start, NULL };
}
heap_bulk_blocks.start = (struct heap_block) { 0, NULL, &heap_bulk_blocks.end };
heap_bulk_blocks.end = (struct heap_block) { 0, &heap_bulk_blocks.start, NULL };
variable_block_update_required = false;
for (int i = 0; i < ARRAY_SIZE(builtin_cache); i++)
{
builtin_cache[i] = NULL;
}
}
static void init_stash()
{
for (int i = 0; i < ARRAY_SIZE(heap_stash); i++) {
heap_stash[i].start =
(struct heap_block) { 0, NULL, &heap_stash[i].end };
heap_stash[i].end =
(struct heap_block) { 0, &heap_stash[i].start, NULL };
}
}
static void heap_check(const char *name, struct heap_blocks *blocks)
{
struct heap_block *start = &blocks->start;
struct heap_block *end = &blocks->end;
for (struct heap_block *b = start->next, *prev = start; b != end; prev = b, b = b->next) {
if (prev == NULL || b == NULL || b->prev != prev) {
opa_abort(name);
}
}
for (struct heap_block *b = end->prev, *next = end; b != start; next = b, b = b->prev) {
if (next == NULL || b == NULL || b->next != next) {
opa_abort(name);
}
}
}
// NOTE(sr): In internal/compiler/wasm, we append segments to the data section.
// Since our memory layout is
// | <-- stack | -- data -- | heap --> |
// we need to adjust the border between data and heap, i.e., where the heap
// starts. When initializing a module, the Start function emitted by the
// compiler will call this function with the new heap base.
OPA_INTERNAL
void opa_malloc_init(unsigned int heap_base)
{
heap_ptr = heap_base;
heap_top = __builtin_wasm_memory_grow(0, 0) * WASM_PAGE_SIZE;
init_free();
init_stash();
}
void opa_malloc_init_test(void)
{
opa_malloc_init(__heap_base);
}
static struct heap_block * __opa_malloc_reuse_fixed(struct heap_blocks *blocks);
static struct heap_block * __opa_malloc_reuse_varying(struct heap_blocks *blocks, size_t size);
static void move_blocks(struct heap_blocks *dst, struct heap_blocks *src);
void opa_free_bulk_commit(void);
WASM_EXPORT(opa_heap_ptr_get)
unsigned int opa_heap_ptr_get(void)
{
return heap_ptr;
}
unsigned int opa_heap_top_get(void)
{
return heap_top;
}
WASM_EXPORT(opa_heap_ptr_set)
void opa_heap_ptr_set(unsigned int ptr)
{
heap_ptr = ptr;
init_free();
}
OPA_INTERNAL
void move_freelists(struct heap_blocks *dst_block_list,
struct heap_blocks *src_block_list,
const char *caller_fail_msg)
{
/*
* First verify that dst freelists are empty and
* all blocks in the src are below the current heap pointer.
*/
for (int i = 0; i < ARRAY_SIZE(heap_free); i++)
{
struct heap_blocks *dst = &dst_block_list[i];
struct heap_blocks *src = &src_block_list[i];
if (dst->start.next != &dst->end || dst->end.prev != &dst->start)
opa_abort(caller_fail_msg);
if (src->end.prev != &src->start) {
struct heap_block *b = src->end.prev;
if ((unsigned int)b + b->size + sizeof(struct heap_block) > heap_ptr)
opa_abort(caller_fail_msg);
}
}
/* Now move the blocks en masse from one freelist to the other. */
for (int i = 0; i < ARRAY_SIZE(heap_free); i++)
move_blocks(&dst_block_list[i], &src_block_list[i]);
}
WASM_EXPORT(opa_heap_blocks_stash)
void opa_heap_blocks_stash(void)
{
move_freelists(heap_stash, heap_free,
"opa_heap_blocks_stash() consistency check failed");
/* clean up dangling references */
init_free();
}
WASM_EXPORT(opa_heap_stash_clear)
void opa_heap_stash_clear(void)
{
init_stash();
}
WASM_EXPORT(opa_heap_blocks_restore)
void opa_heap_blocks_restore(void)
{
move_freelists(heap_free, heap_stash,
"opa_heap_blocks_restore() consistency check failed");
/* clean up dangling references */
init_stash();
}
void opa_heap_top_set(unsigned int top)
{
heap_top = top;
init_free();
}
// returns the free list applicable for the requested size.
static struct heap_blocks * __opa_blocks(size_t size) {
for (int i = 0; i < ARRAY_SIZE(heap_free)-1; i++) {
struct heap_blocks *candidate = &heap_free[i];
if (size <= candidate->size)
{
return candidate;
}
}
return &heap_free[ARRAY_SIZE(heap_free)-1];
}
static void *__opa_malloc_new_allocation(size_t size)
{
unsigned int ptr = heap_ptr;
size_t block_size = sizeof(struct heap_block) + size;
heap_ptr += block_size;
if (heap_ptr >= heap_top)
{
unsigned int pages = (block_size / WASM_PAGE_SIZE) + 1;
if (__builtin_wasm_memory_grow(0, pages) == -1 )
{
opa_abort("opa_malloc: failed");
};
heap_top += (pages * WASM_PAGE_SIZE);
}
struct heap_block *b = (void *)ptr;
b->size = size;
b->prev = NULL;
b->next = NULL;
return b->data;
}
WASM_EXPORT(opa_malloc)
void *opa_malloc(size_t size)
{
// Look for the first free block that is large enough. Split the found block if necessary.
struct heap_blocks *blocks = __opa_blocks(size);
HEAP_CHECK(blocks);
struct heap_block *b = blocks->fixed_size ?
__opa_malloc_reuse_fixed(blocks) : __opa_malloc_reuse_varying(blocks, size);
if (b != NULL)
{
return b->data;
}
// Allocate a new block.
if (blocks->fixed_size)
{
size = blocks->size;
}
return __opa_malloc_new_allocation(size);
}
// returns a free block from the list, if available.
static struct heap_block * __opa_malloc_reuse_fixed(struct heap_blocks *blocks)
{
struct heap_block *end = &blocks->end;
struct heap_block *b = blocks->start.next;
if (b != end)
{
b->prev->next = b->next;
b->next->prev = b->prev;
b->prev = NULL;
b->next = NULL;
HEAP_CHECK(blocks);
return b;
}
return NULL;
}
// finds a free block at least of given size, splitting the found block if the remaining block exceeds the minimum size.
static struct heap_block * __opa_malloc_reuse_varying(struct heap_blocks *blocks, size_t size)
{
struct heap_block *start = &blocks->start;
struct heap_block *end = &blocks->end;
size_t min_size = blocks->size;
if (variable_block_update_required)
opa_free_bulk_commit();
for (struct heap_block *b = start->next; b != end; b = b->next)
{
if (b->size >= (sizeof(struct heap_block) + min_size + size))
{
struct heap_block *remaining = (void *)(&b->data[0]) + size;
remaining->size = b->size - (sizeof(struct heap_block) + size);
remaining->prev = b->prev;
remaining->next = b->next;
remaining->prev->next = remaining;
remaining->next->prev = remaining;
b->size = size;
b->prev = NULL;
b->next = NULL;
HEAP_CHECK(blocks);
return b;
} else if (b->size >= size)
{
b->prev->next = b->next;
b->next->prev = b->prev;
b->prev = NULL;
b->next = NULL;
HEAP_CHECK(blocks);
return b;
}
}
return NULL;
}
WASM_EXPORT(opa_free)
void opa_free(void *ptr)
{
struct heap_block *block = ptr - sizeof(struct heap_block);
#ifdef DEBUG
if (ptr == NULL)
{
opa_abort("opa_free: null pointer");
}
if (block->prev != NULL || block->next != NULL)
{
opa_abort("opa_free: double free");
}
#endif
struct heap_blocks *blocks = __opa_blocks(block->size);
struct heap_block *start = &blocks->start;
struct heap_block *end = &blocks->end;
bool fixed_size = blocks->fixed_size;
HEAP_CHECK(blocks);
// Find the free block available just before this block and try to
// defragment, by trying to merge with this block with the found
// block and the one after.
struct heap_block *prev = start;
if (!fixed_size)
{
for (struct heap_block *b = prev->next; b < block && b != end; prev = b, b = b->next);
struct heap_block *prev_end = (void *)(&prev->data[0]) + prev->size;
struct heap_block *block_end = (void *)(&block->data[0]) + block->size;
if (prev_end == block)
{
prev->size += sizeof(struct heap_block) + block->size;
prev_end = (void *)(&prev->data[0]) + prev->size;
if (prev_end == prev->next) {
struct heap_block *next = prev->next;
prev->size += sizeof(struct heap_block) + next->size;
prev->next = next->next;
prev->next->prev = prev;
}
return;
}
if (block_end == prev->next)
{
struct heap_block *next = prev->next;
block->prev = prev;
block->next = next->next;
block->size += sizeof(struct heap_block) + next->size;
prev->next = block;
block->next->prev = block;
return;
}
}
// List the block as free.
block->prev = prev;
block->next = prev->next;
prev->next = block;
block->next->prev = block;
}
void *opa_realloc(void *ptr, size_t size)
{
struct heap_block *block = ptr - sizeof(struct heap_block);
void *p = opa_malloc(size);
memcpy(p, ptr, block->size < size ? block->size : size);
opa_free(ptr);
return p;
}
static void **__opa_builtin_cache(size_t i)
{
if (i >= ARRAY_SIZE(builtin_cache))
{
opa_abort("opa_malloc: illegal builtin cache index");
}
return &builtin_cache[i];
}
void *opa_builtin_cache_get(size_t i)
{
return *__opa_builtin_cache(i);
}
void opa_builtin_cache_set(size_t i, void *p)
{
*__opa_builtin_cache(i) = p;
}
// Count the number of free blocks. This is for testing only.
size_t opa_heap_free_blocks(void)
{
size_t blocks1 = 0, blocks2 = 0;
for (int i = 0; i < ARRAY_SIZE(heap_free); i++)
{
for (struct heap_block *b = heap_free[i].start.next; b != &heap_free[i].end; b = b->next, blocks1++);
for (struct heap_block *b = heap_free[i].end.prev; b != &heap_free[i].start; b = b->prev, blocks2++);
if (blocks1 != blocks2)
{
opa_abort("opa_malloc: corrupted heap");
}
HEAP_CHECK(&heap_free[i]);
}
return blocks1;
}
static bool blocks_empty(struct heap_blocks *blocks)
{
return blocks->start.next == &blocks->end;
}
static void init_blocks(struct heap_blocks *blocks)
{
blocks->start = (struct heap_block) { 0, NULL, &blocks->end };
blocks->end = (struct heap_block) { 0, &blocks->start, NULL };
}
static void remove_block(struct heap_block *block)
{
block->prev->next = block->next;
block->next->prev = block->prev;
block->prev = NULL;
block->next = NULL;
}
static void append_block(struct heap_blocks *blocks, struct heap_block *block)
{
block->prev = blocks->end.prev;
block->next = &blocks->end;
block->prev->next = block;
block->next->prev = block;
}
static void prepend_block(struct heap_blocks *blocks, struct heap_block *block)
{
block->prev = &blocks->start;
block->next = blocks->start.next;
block->prev->next = block;
block->next->prev = block;
}
static void move_blocks(struct heap_blocks *dst, struct heap_blocks *src)
{
dst->start.prev = NULL; /* unnecessary, but safe */
dst->start.next = src->start.next;
dst->start.next->prev = &dst->start;
dst->end.prev = src->end.prev;
dst->end.next = NULL; /* unnecessary, but safe */
dst->end.prev->next = &dst->end;
/* Fix dangling references in src for consistency */
src->start.next = &src->end;
src->end.prev = &src->start;
}
static void merge_or_append_block(struct heap_blocks *dst, struct heap_block *block)
{
struct heap_block *last = dst->end.prev;
struct heap_block *last_end = (void *)(&last->data[0]) + last->size;
if (last != &dst->start && last_end == block)
last->size += sizeof(struct heap_block) + block->size;
else
append_block(dst, block);
}
static void merge_or_append_blocks(struct heap_blocks *dst, struct heap_blocks *src)
{
while (!blocks_empty(src))
{
struct heap_block *block = src->start.next;
remove_block(block);
merge_or_append_block(dst, block);
}
}
/*
* Assumes list1 and list2 are in order. Merge them into dst in order.
* dst, list1 and list2 must all be different block lists. Assumes dst has
* been initialized.
*
* As a special case, if two blocks being merged are adjacent, combine them
* into a single block.
*/
static void merge_blocks(struct heap_blocks *dst, struct heap_blocks *list1,
struct heap_blocks *list2)
{
while (!blocks_empty(list1) && !blocks_empty(list2)) {
struct heap_block *b1 = list1->start.next;
struct heap_block *b2 = list2->start.next;
struct heap_block *min = (unsigned int)b1 < (unsigned int)b2 ? b1 : b2;
remove_block(min);
merge_or_append_block(dst, min);
}
/* at most one list still has blocks */
if (!blocks_empty(list1))
merge_or_append_blocks(dst, list1);
else
merge_or_append_blocks(dst, list2);
}
/*
* Split a list of blocks into two by alternately appending the blocks
* to two separate block lists. Assumes dst[0] and dst[1] have been initialized.
*/
static void split_blocks(struct heap_blocks dst[2], struct heap_blocks *src)
{
unsigned int i = 0;
while (!blocks_empty(src)) {
struct heap_block *block = src->start.next;
remove_block(block);
append_block(&dst[i], block);
i ^= 1;
}
}
/* Merge sort the blocks on a list in ascending address order */
void merge_sort_blocks(struct heap_blocks *blocks)
{
struct heap_blocks hold[2];
struct heap_block *first;
struct heap_block *second;
/* list length == 0: done */
if (blocks_empty(blocks))
return;
first = blocks->start.next;
second = first->next;
/* list length == 1: done */
if (second == &blocks->end)
return;
/* list length == 2: optimization -- fast block swap+merge */
if (second->next == &blocks->end)
{
if ((unsigned int)first > (unsigned int)second)
{
remove_block(first);
/* blocks now just has 'second' to which we append or merge 'first' */
merge_or_append_block(blocks, first);
}
/* first and second are in order. See if we can merge them. */
else if (((void *)(&first->data[0]) + first->size == second))
{
remove_block(second);
first->size += sizeof(struct heap_block) + second->size;
}
/* one way or the other, we're done */
return;
}
/* list length > 2: recursive case */
for (int i = 0; i < 2; i++)
init_blocks(&hold[i]);
split_blocks(hold, blocks);
merge_sort_blocks(&hold[0]);
merge_sort_blocks(&hold[1]);
merge_blocks(blocks, &hold[0], &hold[1]);
}
static void block_order_check(struct heap_blocks *blocks)
{
struct heap_block *b;
struct heap_block *prev;
struct heap_block *prev_end;
for (prev = NULL, b = blocks->start.next ; b != &blocks->end; prev = b, b = b->next)
{
if (prev == NULL)
continue;
prev_end = (void *)(&prev->data[0]) + prev->size;
if ((unsigned int)prev >= (unsigned int)b)
opa_abort("block_order_check() out of order blocks detected");
if (prev_end > b)
opa_abort("block_order_check() overlapping blocks detected");
if (!blocks->fixed_size && prev_end == b)
opa_abort("block_order_check() unmerged block detected");
}
}
#ifndef DEBUG
#define BLOCK_ORDER_CHECK(blocks)
#else /* DEBUG */
#define BLOCK_ORDER_CHECK(blocks) block_order_check(blocks)
#endif /* DEBUG */
/*
* Save heap blocks to temporary block lists in arbitrary order.
* Later, in opa_free_bulk_commit() release them correctly back to the heap.
*/
void opa_free_bulk(void *ptr)
{
struct heap_block *block = ptr - sizeof(struct heap_block);
struct heap_blocks *blocks = __opa_blocks(block->size);
#ifdef DEBUG
if (ptr == NULL)
{
opa_abort("opa_free_bulk: null pointer");
}
if (block->prev != NULL || block->next != NULL)
{
opa_abort("opa_free_bulk: double free");
}
#endif
if (blocks->fixed_size) {
prepend_block(blocks, block);
HEAP_CHECK(blocks);
} else {
prepend_block(&heap_bulk_blocks, block);
HEAP_CHECK(&heap_bulk_blocks);
variable_block_update_required = true;
}
}
/*
* Return the variable-sized blocks released by opa_free_bulk() to the heap.
* opa_free_bulk() placed the blocks on a list but disregarded
* address order unlike what is done in the heap. This makes freeing K objects
* take O(K) time. Now, to return them to the heap, we need to put them
* in address order along with the other blocks on the heap. This takes
* O(K log K) time where K == N + <number of free blocks>.
*
* This is in contrast to iterative calls to opa_free(). Each call to opa_free()
* takes a worst-case of O(N) time due to the time it takes to linearly insert
* the block into the list. Calling opa_free() iteratively over N objects,
* threfore, takes time that grows in O(N^2). (Even with an empty freelist,
* the average length of the search is O(N/2)).
*
* This function should generally be private. However it is exposed in the
* malloc.h header in case there is a specific desire to ensure predictability
* of allocation time after some bulk free operations. It is also useful
* for tests.
*/
void opa_free_bulk_commit(void)
{
struct heap_blocks *var_blocks = &heap_free[VARIABLE_SIZED_BLOCK_IDX];
struct heap_blocks hold;
merge_sort_blocks(&heap_bulk_blocks);
BLOCK_ORDER_CHECK(&heap_bulk_blocks);
/*
* Need to move variable blocks to new list because merge_blocks()
* expects three distinct lists.
*/
init_blocks(&hold);
move_blocks(&hold, var_blocks);
merge_blocks(var_blocks, &heap_bulk_blocks, &hold);
#ifdef DEBUG
if (!blocks_empty(&heap_bulk_blocks))
opa_abort("Unmerged bulk blocks in heap_bulk_blocks");
if (!blocks_empty(&hold))
opa_abort("Unmerged heap blocks in heap_bulk_blocks");
#endif /* DEBUG */
HEAP_CHECK(var_blocks);
BLOCK_ORDER_CHECK(var_blocks);
variable_block_update_required = false;
}