diff --git a/README.md b/README.md index e96bb16f..893c406c 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,9 @@ A few early examples to get an idea: * **[PSON decoder](./examples/pson)**
A PSON decoder implemented in AssemblyScript. +* **[TLSF memory allocator](./examples/tlsf)**
+ An early port of TLSF to AssemblyScript. + Or browse the [compiler tests](./tests/compiler) for a more in-depth overview of what's supported already. One of them is a [showcase](./tests/compiler/showcase.ts). Getting started diff --git a/bin/README.md b/bin/README.md new file mode 100644 index 00000000..c8c2088f --- /dev/null +++ b/bin/README.md @@ -0,0 +1 @@ +Compiler frontend for node.js. diff --git a/examples/tlsf/README.md b/examples/tlsf/README.md new file mode 100644 index 00000000..6af330a7 --- /dev/null +++ b/examples/tlsf/README.md @@ -0,0 +1,20 @@ +TLSF +==== + +A port of [Matt Conte's implementation](https://github.com/mattconte/tlsf) of the [TLSF](http://www.gii.upv.es/tlsf/) memory allocator to AssemblyScript. + +Instructions +------------ + +To build [assembly/tlsf.ts](./assembly/tlsf.ts) to an untouched and an optimized `.wasm` including their respective `.wast` representations, run: + +``` +$> npm run build +``` + +Afterwards, to run the included [test](./tests/index.js): + +``` +$> npm install +$> npm test +``` diff --git a/examples/tlsf/assembly/tlsf.ts b/examples/tlsf/assembly/tlsf.ts new file mode 100644 index 00000000..da1ddaa7 --- /dev/null +++ b/examples/tlsf/assembly/tlsf.ts @@ -0,0 +1,540 @@ +// based on https://github.com/mattconte/tlsf (BSD) + +type size_t = u32; + +// TLSF achieves O(1) cost for malloc and free operations by limiting +// the search for a free block to a free list of guaranteed size +// adequate to fulfill the request, combined with efficient free list +// queries using bitmasks and architecture-specific bit-manipulation +// routines. +// +// NOTE: TLSF spec relies on ffs/fls returning values 0..31 with -1 +// indicating that no bits are set. In WebAssembly, ctz and clz return +// 32/64 if no bits are set. + +function ffs(word: i32): i32 { + return word ? ctz(word) : -1; +} + +function fls(word: T): i32 { + return (sizeof() << 3) - clz(word) - 1; +} + +const SL_INDEX_COUNT_LOG2: u32 = 5; + +const ALIGN_SIZE_LOG2: u32 = sizeof() == 8 ? 3 : 2; +const ALIGN_SIZE: u32 = (1 << ALIGN_SIZE_LOG2); +const FL_INDEX_MAX: u32 = sizeof() == 8 ? 32 : 30; +const SL_INDEX_COUNT: u32 = (1 << SL_INDEX_COUNT_LOG2); +const FL_INDEX_SHIFT: u32 = SL_INDEX_COUNT_LOG2 + ALIGN_SIZE_LOG2; +const FL_INDEX_COUNT: u32 = FL_INDEX_MAX - FL_INDEX_SHIFT + 1; +const SMALL_BLOCK_SIZE: u32 = 1 << FL_INDEX_SHIFT; + +assert(sizeof() * 8 >= SL_INDEX_COUNT); +assert(ALIGN_SIZE == SMALL_BLOCK_SIZE / SL_INDEX_COUNT); + +/** + * Block header structure. + * + * There are several implementation subtleties involved: + * - The prev_phys_block field is only valid if the previous block is free. + * - The prev_phys_block field is actually stored at the end of the + * previous block. It appears at the beginning of this structure only to + * simplify the implementation. + * - The next_free / prev_free fields are only valid if the block is free. + */ +@explicit +class block_header_t { + + /* Points to the previous physical block. */ + prev_phys_block: block_header_t; + /* The size of this block, excluding the block header. */ + size: size_t; + /* Next free block. */ + next_free: block_header_t; + /* Previous free block. */ + prev_free: block_header_t; +} +const sizeof_block_header_t: usize = 3 * sizeof() * sizeof(); + +// Since block sizes are always at least a multiple of 4, the two least +// significant bits of the size field are used to store the block status: +// - bit 0: whether block is busy or free +// - bit 1: whether previous block is busy or free +const block_header_free_bit: size_t = 1 << 0; +const block_header_prev_free_bit: size_t = 1 << 1; + +// The size of the block header exposed to used blocks is the size field. +// The prev_phys_block field is stored *inside* the previous free block. +const block_header_overhead: size_t = sizeof(); + +// User data starts directly after the size field in a used block. +const block_start_offset: size_t = sizeof() + sizeof(); + +// A free block must be large enough to store its header minus the size of +// the prev_phys_block field, and no larger than the number of addressable +// bits for FL_INDEX. +const block_size_min: size_t = sizeof_block_header_t - sizeof(); +const block_size_max: size_t = 1 << FL_INDEX_MAX; + +/* The TLSF control structure. */ +@explicit +class control_t extends block_header_t { // Empty lists point at this block to indicate they are free. + + /* First level free list bitmap. */ + fl_bitmap: u32; + /** Second level free list bitmaps. */ + sl_bitmap(fl_index: u32): u32 { + const offset: usize = sizeof_block_header_t + sizeof(); + return load(changetype(this) + offset + fl_index * sizeof()); + } + sl_bitmap_set(fl_index: u32, sl_map: u32): void { + const offset: usize = sizeof_block_header_t + sizeof(); + return store(changetype(this) + offset + fl_index * sizeof(), sl_map); + } + /** Head of free lists. */ + blocks(fl_index: u32, sl_index: u32): block_header_t { + const offset: usize = sizeof_block_header_t + sizeof() * FL_INDEX_COUNT * sizeof(); + return load(changetype(this) + offset + (fl_index * SL_INDEX_COUNT + sl_index) * sizeof()); + } + blocks_set(fl_index: u32, sl_index: u32, block: block_header_t): void { + const offset: usize = sizeof_block_header_t + sizeof() * FL_INDEX_COUNT * sizeof(); + return store(changetype(this) + offset + (fl_index * SL_INDEX_COUNT + sl_index) * sizeof(), block); + } +} +const sizeof_control_t: usize = sizeof_block_header_t + (1 + FL_INDEX_COUNT) * sizeof() + FL_INDEX_COUNT * SL_INDEX_COUNT * sizeof(); + +// block_header_t member functions. + +function block_size(block: block_header_t): size_t { + return block.size & ~(block_header_free_bit | block_header_prev_free_bit); +} + +function block_set_size(block: block_header_t, size: size_t): void { + var oldsize = block.size; + block.size = size | (oldsize & (block_header_free_bit | block_header_prev_free_bit)); +} + +function block_is_last(block: block_header_t): bool { + return block_size(block) == 0; +} + +function block_is_free(block: block_header_t): bool { + return (block.size & block_header_free_bit) == block_header_free_bit; +} + +function block_set_free(block: block_header_t): void { + block.size |= block_header_free_bit; +} + +function block_set_used(block: block_header_t): void { + block.size &= ~block_header_free_bit; +} + +function block_is_prev_free(block: block_header_t): bool { + return (block.size & block_header_prev_free_bit) == block_header_prev_free_bit; +} + +function block_set_prev_free(block: block_header_t): void { + block.size |= block_header_prev_free_bit; +} + +function block_set_prev_used(block: block_header_t): void { + block.size &= ~block_header_prev_free_bit; +} + +function block_from_ptr(ptr: usize): block_header_t { + return changetype(ptr - block_start_offset); +} + +function block_to_ptr(block: block_header_t): usize { + return changetype(block) + block_start_offset; +} + +/* Return location of next block after block of given size. */ +function offset_to_block(ptr: usize, size: size_t): block_header_t { + return changetype(ptr + size); +} + +/* Return location of previous block. */ +function block_prev(block: block_header_t): block_header_t { + assert(block_is_prev_free(block), "previous block must be free"); + return block.prev_phys_block; +} + +/* Return location of next existing block. */ +function block_next(block: block_header_t): block_header_t { + var next = offset_to_block(block_to_ptr(block), block_size(block) - block_header_overhead); + assert(!block_is_last(block)); + return next; +} + +/* Link a new block with its physical neighbor, return the neighbor. */ +function block_link_next(block: block_header_t): block_header_t { + var next = block_next(block); + next.prev_phys_block = block; + return next; +} + +function block_mark_as_free(block: block_header_t): void { + // Link the block to the next block, first. + var next = block_link_next(block); + block_set_prev_free(next); + block_set_free(block); +} + +function block_mark_as_used(block: block_header_t): void { + var next = block_next(block); + block_set_prev_used(next); + block_set_used(block); +} + +function align_up(x: size_t, align: size_t): size_t { + assert(0 == (align & (align - 1)), "must align to a power of two"); + return (x + (align - 1)) & ~(align - 1); +} + +function align_down(x: size_t, align: size_t): size_t { + assert(0 == (align & (align - 1)), "must align to a power of two"); + return x - (x & (align - 1)); +} + +function align_ptr(ptr: usize, align: size_t): usize { + var aligned = (ptr + (align - 1)) & ~(align - 1); + assert(0 == (align & (align - 1)), "must align to a power of two"); + return aligned; +} + +/** + * Adjust an allocation size to be aligned to word size, and no smaller + * than internal minimum. + */ +function adjust_request_size(size: size_t, align: size_t): size_t { + var adjust: size_t = 0; + if (size) { + var aligned = align_up(size, align); + // aligned sized must not exceed block_size_max or we'll go out of bounds on sl_bitmap + if (aligned < block_size_max) { + adjust = max(aligned, block_size_min); + } + } + return adjust; +} + +// TLSF utility functions. In most cases, these are direct translations of +// the documentation found in the white paper. + +var fl_out: i32, sl_out: i32; + +function mapping_insert(size: size_t): void { + var fl: i32, sl: i32; + if (size < SMALL_BLOCK_SIZE) { + // Store small blocks in first list. + fl = 0; + sl = size / (SMALL_BLOCK_SIZE / SL_INDEX_COUNT); + } else { + fl = fls(size); + sl = (size >> (fl - SL_INDEX_COUNT_LOG2)) ^ (1 << SL_INDEX_COUNT_LOG2); + fl -= (FL_INDEX_SHIFT - 1); + } + fl_out = fl; + sl_out = sl; +} + +function mapping_search(size: size_t): void { + if (size >= SMALL_BLOCK_SIZE) { + var round: size_t = (1 << (fls(size) - SL_INDEX_COUNT_LOG2)) - 1; + size += round; + } + mapping_insert(size); +} + +function search_suitable_block(control: control_t, fl: i32, sl: i32): block_header_t { + var sl_map = control.sl_bitmap(fl) & (~0 << sl); + if (!sl_map) { + // No block exists. Search in the next largest first-level list. + var fl_map = control.fl_bitmap & (~0 << (fl + 1)); + if (!fl_map) { + // No free blocks available, memory has been exhausted. + return changetype(0); + } + fl = ffs(fl_map); + fl_out = fl; + sl_map = control.sl_bitmap(fl); + } + assert(sl_map, "internal error - second level bitmap is null"); + + sl = ffs(sl_map); + sl_out = sl; + return control.blocks(fl, sl); +} + +function remove_free_block(control: control_t, block: block_header_t, fl: i32, sl: i32): void { + var prev = block.prev_free; + var next = block.next_free; + assert(prev, "prev_free field can not be null"); + assert(next, "next_free field can not be null"); + next.prev_free = prev; + prev.next_free = next; + + if (control.blocks(fl, sl) == block) { + control.blocks_set(fl, sl, next); + if (next == control) { + control.sl_bitmap_set(fl, control.sl_bitmap(fl) & ~(1 << sl)); + if (!control.sl_bitmap(fl)) { + control.fl_bitmap &= ~(1 << fl); + } + } + } +} + +function insert_free_block(control: control_t, block: block_header_t, fl: i32, sl: i32): void { + var current = control.blocks(fl, sl); + + assert(current, "free list cannot have a null entry"); + assert(block, "cannot insert a null entry into the free list"); + + block.next_free = current; + block.prev_free = control; + current.prev_free = block; + + assert(block_to_ptr(block) == align_ptr(block_to_ptr(block), ALIGN_SIZE), "block not aligned properly"); + + control.blocks_set(fl, sl, block); + control.fl_bitmap |= (1 << fl); + control.sl_bitmap_set(fl, control.sl_bitmap(fl) | (1 << sl)) +} + + +function block_remove(control: control_t, block: block_header_t): void { + mapping_insert(block_size(block)); + remove_free_block(control, block, fl_out, sl_out); +} + +function block_insert(control: control_t, block: block_header_t): void { + mapping_insert(block_size(block)); + insert_free_block(control, block, fl_out, sl_out); +} + +function block_can_split(block: block_header_t, size: size_t): bool { + return block_size(block) >= sizeof_block_header_t + size; +} + +function block_split(block: block_header_t, size: size_t): block_header_t { + var remaining = offset_to_block(block_to_ptr(block), size - block_header_overhead); + var remain_size = block_size(block) - (size + block_header_overhead); + + assert(block_to_ptr(remaining) == align_ptr(block_to_ptr(remaining), ALIGN_SIZE), "remaining block not aligned properly"); + assert(block_size(block) == remain_size + size + block_header_overhead); + + block_set_size(remaining, remain_size); + + assert(block_size(remaining) >= block_size_min, "block split with invalid size"); + + block_set_size(block, size); + block_mark_as_free(remaining); + return remaining; +} + +function block_absorb(prev: block_header_t, block: block_header_t): block_header_t { + assert(!block_is_last(prev), "previous block can't be last"); + prev.size += block_size(block) + block_header_overhead; + block_link_next(prev); + return prev; +} + +/* Merge a just-freed block with an adjacent previous free block. */ +function block_merge_prev(control: control_t, block: block_header_t): block_header_t { + if (block_is_prev_free(block)) { + var prev = block_prev(block); + assert(prev, "prev physical block can't be null"); + assert(block_is_free(prev), "prev block is not free though marked as such"); + block_remove(control, prev); + block = block_absorb(prev, block); + } + return block; +} + +/* Merge a just-freed block with an adjacent free block. */ +function block_merge_next(control: control_t, block: block_header_t): block_header_t { + var next: block_header_t = block_next(block); + assert(next, "next physical block can't be null"); + if (block_is_free(next)) { + assert(!block_is_last(block), "previous block can't be last"); + block_remove(control, next); + block = block_absorb(block, next); + } + return block; +} + +/* Trim any trailing block space off the end of a block, return to pool. */ +function block_trim_free(control: control_t, block: block_header_t, size: size_t): void { + assert(block_is_free(block), "block must be free"); + if (block_can_split(block, size)) { + var remaining_block = block_split(block, size); + block_link_next(block); + block_set_prev_free(remaining_block); + block_insert(control, remaining_block); + } +} + +/* Trim any trailing block space off the end of a used block, return to pool. */ +function block_trim_used(control: control_t, block: block_header_t, size: size_t): void { + assert(!block_is_free(block), "block must be used"); + if (block_can_split(block, size)) { + // If the next block is free, we must coalesce + var remaining_block = block_split(block, size); + block_set_prev_used(remaining_block); + remaining_block = block_merge_next(control, remaining_block); + block_insert(control, remaining_block); + } +} + +function block_trim_free_leading(control: control_t, block: block_header_t, size: size_t): block_header_t { + var remaining_block = block; + if (block_can_split(block, size)) { + remaining_block = block_split(block, size - block_header_overhead); + block_set_prev_free(remaining_block); + block_link_next(block); + block_insert(control, block); + } + return remaining_block; +} + +function block_locate_free(control: control_t, size: size_t): block_header_t { + var index: u64 = 0; + var block: block_header_t = changetype(0); + if (size) { + mapping_search(size); + if (fl_out < FL_INDEX_MAX) { + block = search_suitable_block(control, fl_out, sl_out); + } + } + if (block) { + assert(block_size(block) >= size); + remove_free_block(control, block, fl_out, sl_out); + } + return block; +} + +function block_prepare_used(control: control_t, block: block_header_t, size: size_t): usize { + var p: usize = 0; + if (block) { + assert(size, "size must be non-zero"); + block_trim_free(control, block, size); + block_mark_as_used(block); + p = block_to_ptr(block); + } + return p; +} + +/* Clear structure and point all empty lists at the null block. */ +function control_construct(control: control_t): void { + control.next_free = control; + control.prev_free = control; + control.fl_bitmap = 0; + for (var i = 0; i < FL_INDEX_COUNT; ++i) { + control.sl_bitmap_set(i, 0); + for (var j = 0; j < SL_INDEX_COUNT; ++j) { + control.blocks_set(i, j, control); + } + } +} + +type tlsf_t = usize; +type pool_t = usize; + +var TLSF: tlsf_t = 0; + +function create(mem: usize): tlsf_t { + if ((mem % ALIGN_SIZE) != 0) + throw new Error("Memory must be aligned"); + control_construct(changetype(mem)); + return mem; +} + +function create_with_pool(mem: usize, bytes: size_t): tlsf_t { + var tlsf = create(mem); + add_pool(tlsf, mem + sizeof_control_t, bytes - sizeof_control_t); + return tlsf; +} + +function add_pool(tlsf: tlsf_t, mem: usize, bytes: size_t): pool_t { + var block: block_header_t; + var next: block_header_t; + + const pool_overhead: size_t = 2 * block_header_overhead; + var pool_bytes = align_down(bytes - pool_overhead, ALIGN_SIZE); + if ((mem % ALIGN_SIZE) != 0) + throw new Error("Memory must be aligned"); + if (pool_bytes < block_size_min || pool_bytes > block_size_max) + throw new Error("Memory size must be between min and max"); + + // Create the main free block. Offset the start of the block slightly + // so that the prev_phys_block field falls outside of the pool - + // it will never be used. + block = offset_to_block(mem, -block_header_overhead); + block_set_size(block, pool_bytes); + block_set_free(block); + block_set_prev_used(block); + block_insert(changetype(tlsf), block); + + // Split the block to create a zero-size sentinel block. + next = block_link_next(block); + block_set_size(next, 0); + block_set_used(next); + block_set_prev_free(next); + + return mem; +} + +export function allocate_memory(size: size_t): usize { + if (!TLSF) + TLSF = create_with_pool(HEAP_BASE, (current_memory() << 16) - HEAP_BASE); + var control = changetype(TLSF); + var adjust = adjust_request_size(size, ALIGN_SIZE); + var block = block_locate_free(control, adjust); + if (!block && size > 0) { + if (size & 0xffff) + size = (size | 0xffff) + 1; + var oldsize = grow_memory(size >>> 16); + if (oldsize >= 0) { + add_pool(TLSF, oldsize << 16, size); + } else { + throw new Error("Out of memory"); + } + block = block_locate_free(control, adjust); + } + return block_prepare_used(control, block, adjust); +} + +export function free_memory(ptr: usize): void { + if (TLSF && ptr) { + var control = changetype(TLSF); + var block = block_from_ptr(ptr); + assert(!block_is_free(block), "block already marked as free"); + block_mark_as_free(block); + block = block_merge_prev(control, block); + block = block_merge_next(control, block); + block_insert(control, block); + } +} + +function test_ffs_fls(): i32 { + // Verify ffs/fls work properly. + var rv = 0; + rv += (ffs(0) == -1) ? 0 : 0x1; + rv += (fls(0) == -1) ? 0 : 0x2; + rv += (ffs(1) == 0) ? 0 : 0x4; + rv += (fls(1) == 0) ? 0 : 0x8; + rv += (ffs(0x80000000) == 31) ? 0 : 0x10; + rv += (ffs(0x80008000) == 15) ? 0 : 0x20; + rv += (fls(0x80000008) == 31) ? 0 : 0x40; + rv += (fls(0x7FFFFFFF) == 30) ? 0 : 0x80; + rv += (fls(0x80000000) == 31) ? 0 : 0x100; + rv += (fls(0x100000000) == 32) ? 0 : 0x200; + rv += (fls(0xffffffffffffffff) == 63) ? 0 : 0x400; + return rv; +} + +assert(!test_ffs_fls()); diff --git a/examples/tlsf/assembly/tsconfig.json b/examples/tlsf/assembly/tsconfig.json new file mode 100644 index 00000000..6e52b21c --- /dev/null +++ b/examples/tlsf/assembly/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../../std/assembly.json", + "include": [ + "./**/*.ts" + ] +} diff --git a/examples/tlsf/package.json b/examples/tlsf/package.json new file mode 100644 index 00000000..d92ab5e1 --- /dev/null +++ b/examples/tlsf/package.json @@ -0,0 +1,11 @@ +{ + "name": "@assemblyscript/tlsf", + "version": "1.0.0", + "private": true, + "scripts": { + "build": "npm run build:untouched && npm run build:optimized", + "build:untouched": "asc assembly/tlsf.ts -t tlsf.untouched.wast -b tlsf.untouched.wasm --validate", + "build:optimized": "asc -O assembly/tlsf.ts -b tlsf.optimized.wasm -t tlsf.optimized.wast --validate --noAssert", + "test": "node tests" + } +} diff --git a/examples/tlsf/tests/index.js b/examples/tlsf/tests/index.js new file mode 100644 index 00000000..9506d772 --- /dev/null +++ b/examples/tlsf/tests/index.js @@ -0,0 +1,57 @@ +var fs = require("fs"); +var tlsf = new WebAssembly.Instance(WebAssembly.Module(fs.readFileSync(__dirname + "/../tlsf.optimized.wasm")), { + env: { + log_i: function(i) { i == -1 ? console.log("---") : console.log("log_i -> " + i); } + } +}).exports; + +console.log(Object.keys(tlsf)); + +try { + var memSize = 0; + for (var j = 0; j < 500; ++j) { + var ptr; + var ptrs = []; + for (var i = 0; i < 256; ++i) { + var size = i * 64; + ptr = tlsf.allocate_memory(size); + console.log("allocate_memory(" + size + ") = " + ptr); + if (!(i % 4)) { + tlsf.free_memory(ptr); + console.log("free_memory(" + ptr + ")"); + } else + ptrs.push(ptr); + } + while (ptrs.length) { + ptr = Math.random() < 0.5 ? ptrs.pop() : ptrs.shift(); + console.log("free_memory(" + ptr + ")"); + tlsf.free_memory(ptr); + } + if (memSize && memSize != tlsf.memory.length) + throw new Error("memory should not grow multiple times"); + memSize = tlsf.memory.length; + } + var ptr = tlsf.allocate_memory(64); + console.log("allocate_memory(" + 64 + ") = " + ptr); +} catch (e) { + console.log(e.stack); + mem(tlsf.memory); +} + +function mem(memory, offset) { + if (!offset) offset = 0; + var mem = new Uint8Array(memory.buffer, offset); + var stackTop = new Uint32Array(memory.buffer, 4, 1)[0]; + var hex = []; + for (var i = 0; i < 1024; ++i) { + var o = (offset + i).toString(16); + while (o.length < 3) o = "0" + o; + if ((i & 15) === 0) { + hex.push("\n" + o + ":"); + } + var h = mem[i].toString(16); + if (h.length < 2) h = "0" + h; + hex.push(h); + } + console.log(hex.join(" ") + " ..."); +}