zig

fork of https://codeberg.org/ziglang/zig
Log | Files | Refs | README | LICENSE

commit 6ccabbd4e558dd0d56c471408c4cf29e0992dc1c (tree)
parent 6744160211b2ab480e4c479e46440c76f00c1810
Author: Andrew Kelley <andrew@ziglang.org>
Date:   Tue, 10 Feb 2026 19:28:43 -0800

std: brk allocator for single-threaded mode

Diffstat:
Mlib/c/malloc.zig | 6+++---
Mlib/std/heap.zig | 29+++++++++++++++--------------
Alib/std/heap/BrkAllocator.zig | 350+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dlib/std/heap/WasmAllocator.zig | 326-------------------------------------------------------------------------------
Dlib/std/heap/sbrk_allocator.zig | 180-------------------------------------------------------------------------------
Mlib/std/os/linux.zig | 4++++
6 files changed, 372 insertions(+), 523 deletions(-)

diff --git a/lib/c/malloc.zig b/lib/c/malloc.zig @@ -1,7 +1,7 @@ -//! Based on wrapping a stateless Zig Allocator implementation, apropriate for: +//! Based on wrapping a stateless Zig Allocator implementation, appropriate for: //! - ReleaseFast and ReleaseSmall optimization modes, with multi-threading //! enabled. -//! - WebAssembly in single-threaded mode. +//! - WebAssembly or Linux in single-threaded release modes. //! //! Because the libc APIs don't have client alignment and size tracking, in //! order to take advantage of Zig allocator implementations, additional @@ -40,7 +40,7 @@ const no_context: *anyopaque = undefined; const no_ra: usize = undefined; const vtable = switch (builtin.cpu.arch) { .wasm32, .wasm64 => std.heap.WasmAllocator.vtable, - else => std.heap.SmpAllocator.vtable, + else => if (builtin.single_threaded) std.heap.BrkAllocator.vtable else std.heap.SmpAllocator.vtable, }; /// Needed because libc memory allocators don't provide old alignment and size diff --git a/lib/std/heap.zig b/lib/std/heap.zig @@ -13,9 +13,9 @@ pub const ArenaAllocator = @import("heap/arena_allocator.zig").ArenaAllocator; pub const SmpAllocator = @import("heap/SmpAllocator.zig"); pub const FixedBufferAllocator = @import("heap/FixedBufferAllocator.zig"); pub const PageAllocator = @import("heap/PageAllocator.zig"); -pub const SbrkAllocator = @import("heap/sbrk_allocator.zig").SbrkAllocator; pub const ThreadSafeAllocator = @import("heap/ThreadSafeAllocator.zig"); -pub const WasmAllocator = @import("heap/WasmAllocator.zig"); +pub const WasmAllocator = if (builtin.single_threaded) BrkAllocator else @compileError("unimplemented"); +pub const BrkAllocator = @import("heap/BrkAllocator.zig"); pub const DebugAllocatorConfig = @import("heap/debug_allocator.zig").Config; pub const DebugAllocator = @import("heap/debug_allocator.zig").DebugAllocator; @@ -356,9 +356,6 @@ pub const page_allocator: Allocator = if (@hasDecl(root, "os") and else if (builtin.target.cpu.arch.isWasm()) .{ .ptr = undefined, .vtable = &WasmAllocator.vtable, -} else if (builtin.target.os.tag == .plan9) .{ - .ptr = undefined, - .vtable = &SbrkAllocator(std.os.plan9.sbrk).vtable, } else .{ .ptr = undefined, .vtable = &PageAllocator.vtable, @@ -369,16 +366,18 @@ pub const smp_allocator: Allocator = .{ .vtable = &SmpAllocator.vtable, }; -/// This allocator is fast, small, and specific to WebAssembly. In the future, -/// this will be the implementation automatically selected by -/// `GeneralPurposeAllocator` when compiling in `ReleaseSmall` mode for wasm32 -/// and wasm64 architectures. -/// Until then, it is available here to play with. +/// This allocator is fast, small, and specific to WebAssembly. pub const wasm_allocator: Allocator = .{ .ptr = undefined, .vtable = &WasmAllocator.vtable, }; +/// Supports single-threaded WebAssembly and Linux. +pub const brk_allocator: Allocator = .{ + .ptr = undefined, + .vtable = &BrkAllocator.vtable, +}; + /// Returns a `StackFallbackAllocator` allocating using either a /// `FixedBufferAllocator` on an array of size `size` and falling back to /// `fallback_allocator` if that fails. @@ -1014,9 +1013,11 @@ test { _ = GeneralPurposeAllocator; _ = FixedBufferAllocator; _ = ThreadSafeAllocator; - _ = SbrkAllocator; - if (builtin.target.cpu.arch.isWasm()) { - _ = WasmAllocator; + if (builtin.single_threaded) { + if (builtin.cpu.arch.isWasm() or (builtin.os.tag == .linux and !builtin.link_libc)) { + _ = brk_allocator; + } + } else { + _ = smp_allocator; } - if (!builtin.single_threaded) _ = smp_allocator; } diff --git a/lib/std/heap/BrkAllocator.zig b/lib/std/heap/BrkAllocator.zig @@ -0,0 +1,350 @@ +//! Supports single-threaded targets that have a sbrk-like primitive which includes +//! Linux and WebAssembly. +//! +//! On Linux, assumes exclusive access to the brk syscall. +const BrkAllocator = @This(); +const builtin = @import("builtin"); + +const std = @import("../std.zig"); +const Allocator = std.mem.Allocator; +const Alignment = std.mem.Alignment; +const assert = std.debug.assert; +const math = std.math; +const page_size_max = std.heap.page_size_max; + +comptime { + if (!builtin.single_threaded) @compileError("unsupported"); +} + +next_addrs: [size_class_count]usize = @splat(0), +/// For each size class, points to the freed pointer. +frees: [size_class_count]usize = @splat(0), +/// For each big size class, points to the freed pointer. +big_frees: [big_size_class_count]usize = @splat(0), +prev_brk: usize = 0, + +var global: BrkAllocator = .{}; + +pub const vtable: Allocator.VTable = .{ + .alloc = alloc, + .resize = resize, + .remap = remap, + .free = free, +}; + +pub const Error = Allocator.Error; + +const max_usize = math.maxInt(usize); +const ushift = math.Log2Int(usize); +const bigpage_size = 64 * 1024; +const pages_per_bigpage = bigpage_size / page_size_max; +const bigpage_count = max_usize / bigpage_size; + +comptime { + assert(bigpage_size >= page_size_max); +} + +/// Because of storing free list pointers, the minimum size class is 3. +const min_class = math.log2(math.ceilPowerOfTwoAssert(usize, 1 + @sizeOf(usize))); +const size_class_count = math.log2(bigpage_size) - min_class; +/// 0 - 1 bigpage +/// 1 - 2 bigpages +/// 2 - 4 bigpages +/// etc. +const big_size_class_count = math.log2(bigpage_count); + +fn alloc(ctx: *anyopaque, len: usize, alignment: Alignment, return_address: usize) ?[*]u8 { + _ = ctx; + _ = return_address; + // Make room for the freelist next pointer. + const actual_len = @max(len +| @sizeOf(usize), alignment.toByteUnits()); + const slot_size = math.ceilPowerOfTwo(usize, actual_len) catch return null; + const class = math.log2(slot_size) - min_class; + if (class < size_class_count) { + const addr = a: { + const top_free_ptr = global.frees[class]; + if (top_free_ptr != 0) { + const node: *usize = @ptrFromInt(top_free_ptr + (slot_size - @sizeOf(usize))); + global.frees[class] = node.*; + break :a top_free_ptr; + } + + const next_addr = global.next_addrs[class]; + if (next_addr % page_size_max == 0) { + const addr = allocBigPages(1); + if (addr == 0) return null; + //std.debug.print("allocated fresh slot_size={d} class={d} addr=0x{x}\n", .{ + // slot_size, class, addr, + //}); + global.next_addrs[class] = addr + slot_size; + break :a addr; + } else { + global.next_addrs[class] = next_addr + slot_size; + break :a next_addr; + } + }; + return @ptrFromInt(addr); + } + const bigpages_needed = bigPagesNeeded(actual_len); + return @ptrFromInt(allocBigPages(bigpages_needed)); +} + +fn resize( + ctx: *anyopaque, + buf: []u8, + alignment: Alignment, + new_len: usize, + return_address: usize, +) bool { + _ = ctx; + _ = return_address; + // We don't want to move anything from one size class to another, but we + // can recover bytes in between powers of two. + const buf_align = alignment.toByteUnits(); + const old_actual_len = @max(buf.len + @sizeOf(usize), buf_align); + const new_actual_len = @max(new_len +| @sizeOf(usize), buf_align); + const old_small_slot_size = math.ceilPowerOfTwoAssert(usize, old_actual_len); + const old_small_class = math.log2(old_small_slot_size) - min_class; + if (old_small_class < size_class_count) { + const new_small_slot_size = math.ceilPowerOfTwo(usize, new_actual_len) catch return false; + return old_small_slot_size == new_small_slot_size; + } else { + const old_bigpages_needed = bigPagesNeeded(old_actual_len); + const old_big_slot_pages = math.ceilPowerOfTwoAssert(usize, old_bigpages_needed); + const new_bigpages_needed = bigPagesNeeded(new_actual_len); + const new_big_slot_pages = math.ceilPowerOfTwo(usize, new_bigpages_needed) catch return false; + return old_big_slot_pages == new_big_slot_pages; + } +} + +fn remap( + context: *anyopaque, + memory: []u8, + alignment: Alignment, + new_len: usize, + return_address: usize, +) ?[*]u8 { + return if (resize(context, memory, alignment, new_len, return_address)) memory.ptr else null; +} + +fn free( + ctx: *anyopaque, + buf: []u8, + alignment: Alignment, + return_address: usize, +) void { + _ = ctx; + _ = return_address; + const buf_align = alignment.toByteUnits(); + const actual_len = @max(buf.len + @sizeOf(usize), buf_align); + const slot_size = math.ceilPowerOfTwoAssert(usize, actual_len); + const class = math.log2(slot_size) - min_class; + const addr = @intFromPtr(buf.ptr); + if (class < size_class_count) { + const node: *usize = @ptrFromInt(addr + (slot_size - @sizeOf(usize))); + node.* = global.frees[class]; + global.frees[class] = addr; + } else { + const bigpages_needed = bigPagesNeeded(actual_len); + const pow2_pages = math.ceilPowerOfTwoAssert(usize, bigpages_needed); + const big_slot_size_bytes = pow2_pages * bigpage_size; + const node: *usize = @ptrFromInt(addr + (big_slot_size_bytes - @sizeOf(usize))); + const big_class = math.log2(pow2_pages); + node.* = global.big_frees[big_class]; + global.big_frees[big_class] = addr; + } +} + +inline fn bigPagesNeeded(byte_count: usize) usize { + return (byte_count + (bigpage_size + (@sizeOf(usize) - 1))) / bigpage_size; +} + +fn allocBigPages(n: usize) usize { + const pow2_pages = math.ceilPowerOfTwoAssert(usize, n); + const slot_size_bytes = pow2_pages * bigpage_size; + const class = math.log2(pow2_pages); + + const top_free_ptr = global.big_frees[class]; + if (top_free_ptr != 0) { + const node: *usize = @ptrFromInt(top_free_ptr + (slot_size_bytes - @sizeOf(usize))); + global.big_frees[class] = node.*; + return top_free_ptr; + } + + if (builtin.cpu.arch.isWasm()) { + const page_index = @wasmMemoryGrow(0, pow2_pages * pages_per_bigpage); + if (page_index == -1) return 0; + return @as(usize, @intCast(page_index)) * page_size_max; + } else if (builtin.os.tag == .linux) { + const start_brk = s: { + const start_brk = global.prev_brk; + break :s if (start_brk == 0) std.os.linux.brk(0) else start_brk; + }; + const end_brk = start_brk + pow2_pages * pages_per_bigpage * page_size_max; + const new_prev_brk = std.os.linux.brk(end_brk); + global.prev_brk = new_prev_brk; + if (new_prev_brk != end_brk) return 0; + return start_brk; + } else { + @compileError("no sbrk-like OS primitive available"); + } +} + +const test_ally: Allocator = .{ + .ptr = undefined, + .vtable = &vtable, +}; + +test "small allocations - free in same order" { + var list: [513]*u64 = undefined; + + var i: usize = 0; + while (i < 513) : (i += 1) { + const ptr = try test_ally.create(u64); + list[i] = ptr; + } + + for (list) |ptr| { + test_ally.destroy(ptr); + } +} + +test "small allocations - free in reverse order" { + var list: [513]*u64 = undefined; + + var i: usize = 0; + while (i < 513) : (i += 1) { + const ptr = try test_ally.create(u64); + list[i] = ptr; + } + + i = list.len; + while (i > 0) { + i -= 1; + const ptr = list[i]; + test_ally.destroy(ptr); + } +} + +test "large allocations" { + const ptr1 = try test_ally.alloc(u64, 42768); + const ptr2 = try test_ally.alloc(u64, 52768); + test_ally.free(ptr1); + const ptr3 = try test_ally.alloc(u64, 62768); + test_ally.free(ptr3); + test_ally.free(ptr2); +} + +test "very large allocation" { + try std.testing.expectError(error.OutOfMemory, test_ally.alloc(u8, math.maxInt(usize))); +} + +test "realloc" { + var slice = try test_ally.alignedAlloc(u8, .of(u32), 1); + defer test_ally.free(slice); + slice[0] = 0x12; + + // This reallocation should keep its pointer address. + const old_slice = slice; + slice = try test_ally.realloc(slice, 2); + try std.testing.expect(old_slice.ptr == slice.ptr); + try std.testing.expect(slice[0] == 0x12); + slice[1] = 0x34; + + // This requires upgrading to a larger size class + slice = try test_ally.realloc(slice, 17); + try std.testing.expect(slice[0] == 0x12); + try std.testing.expect(slice[1] == 0x34); +} + +test "shrink" { + var slice = try test_ally.alloc(u8, 20); + defer test_ally.free(slice); + + @memset(slice, 0x11); + + try std.testing.expect(test_ally.resize(slice, 17)); + slice = slice[0..17]; + + for (slice) |b| { + try std.testing.expect(b == 0x11); + } + + try std.testing.expect(test_ally.resize(slice, 16)); + slice = slice[0..16]; + + for (slice) |b| { + try std.testing.expect(b == 0x11); + } +} + +test "large object - grow" { + if (builtin.os.tag == .linux) return error.SkipZigTest; + + var slice1 = try test_ally.alloc(u8, bigpage_size * 2 - 20); + defer test_ally.free(slice1); + + const old = slice1; + slice1 = try test_ally.realloc(slice1, bigpage_size * 2 - 10); + try std.testing.expectEqual(slice1.ptr, old.ptr); + + slice1 = try test_ally.realloc(slice1, bigpage_size * 2); + slice1 = try test_ally.realloc(slice1, bigpage_size * 2 + 1); +} + +test "realloc small object to large object" { + var slice = try test_ally.alloc(u8, 70); + defer test_ally.free(slice); + slice[0] = 0x12; + slice[60] = 0x34; + + // This requires upgrading to a large object + const large_object_size = bigpage_size * 2 + 50; + slice = try test_ally.realloc(slice, large_object_size); + try std.testing.expect(slice[0] == 0x12); + try std.testing.expect(slice[60] == 0x34); +} + +test "shrink large object to large object" { + var slice = try test_ally.alloc(u8, bigpage_size * 2 + 50); + defer test_ally.free(slice); + slice[0] = 0x12; + slice[60] = 0x34; + + try std.testing.expect(test_ally.resize(slice, bigpage_size * 2 + 1)); + slice = slice[0 .. bigpage_size * 2 + 1]; + try std.testing.expect(slice[0] == 0x12); + try std.testing.expect(slice[60] == 0x34); + + try std.testing.expect(test_ally.resize(slice, bigpage_size * 2 + 1)); + try std.testing.expect(slice[0] == 0x12); + try std.testing.expect(slice[60] == 0x34); + + slice = try test_ally.realloc(slice, bigpage_size * 2); + try std.testing.expect(slice[0] == 0x12); + try std.testing.expect(slice[60] == 0x34); +} + +test "realloc large object to small object" { + var slice = try test_ally.alloc(u8, bigpage_size * 2 + 50); + defer test_ally.free(slice); + slice[0] = 0x12; + slice[16] = 0x34; + + slice = try test_ally.realloc(slice, 19); + try std.testing.expect(slice[0] == 0x12); + try std.testing.expect(slice[16] == 0x34); +} + +test "objects of size 1024 and 2048" { + const slice = try test_ally.alloc(u8, 1025); + const slice2 = try test_ally.alloc(u8, 3000); + + test_ally.free(slice); + test_ally.free(slice2); +} + +test "standard allocator tests" { + try std.heap.testAllocator(test_ally); + try std.heap.testAllocatorAligned(test_ally); +} diff --git a/lib/std/heap/WasmAllocator.zig b/lib/std/heap/WasmAllocator.zig @@ -1,326 +0,0 @@ -const std = @import("../std.zig"); -const builtin = @import("builtin"); -const Allocator = std.mem.Allocator; -const mem = std.mem; -const assert = std.debug.assert; -const wasm = std.wasm; -const math = std.math; - -comptime { - if (!builtin.target.cpu.arch.isWasm()) { - @compileError("only available for wasm32 arch"); - } - if (!builtin.single_threaded) { - @compileError("TODO implement support for multi-threaded wasm"); - } -} - -pub const vtable: Allocator.VTable = .{ - .alloc = alloc, - .resize = resize, - .remap = remap, - .free = free, -}; - -pub const Error = Allocator.Error; - -const max_usize = math.maxInt(usize); -const ushift = math.Log2Int(usize); -const bigpage_size = 64 * 1024; -const pages_per_bigpage = bigpage_size / wasm.page_size; -const bigpage_count = max_usize / bigpage_size; - -/// Because of storing free list pointers, the minimum size class is 3. -const min_class = math.log2(math.ceilPowerOfTwoAssert(usize, 1 + @sizeOf(usize))); -const size_class_count = math.log2(bigpage_size) - min_class; -/// 0 - 1 bigpage -/// 1 - 2 bigpages -/// 2 - 4 bigpages -/// etc. -const big_size_class_count = math.log2(bigpage_count); - -var next_addrs: [size_class_count]usize = @splat(0); -/// For each size class, points to the freed pointer. -var frees: [size_class_count]usize = @splat(0); -/// For each big size class, points to the freed pointer. -var big_frees: [big_size_class_count]usize = @splat(0); - -fn alloc(ctx: *anyopaque, len: usize, alignment: mem.Alignment, return_address: usize) ?[*]u8 { - _ = ctx; - _ = return_address; - // Make room for the freelist next pointer. - const actual_len = @max(len +| @sizeOf(usize), alignment.toByteUnits()); - const slot_size = math.ceilPowerOfTwo(usize, actual_len) catch return null; - const class = math.log2(slot_size) - min_class; - if (class < size_class_count) { - const addr = a: { - const top_free_ptr = frees[class]; - if (top_free_ptr != 0) { - const node: *usize = @ptrFromInt(top_free_ptr + (slot_size - @sizeOf(usize))); - frees[class] = node.*; - break :a top_free_ptr; - } - - const next_addr = next_addrs[class]; - if (next_addr % wasm.page_size == 0) { - const addr = allocBigPages(1); - if (addr == 0) return null; - //std.debug.print("allocated fresh slot_size={d} class={d} addr=0x{x}\n", .{ - // slot_size, class, addr, - //}); - next_addrs[class] = addr + slot_size; - break :a addr; - } else { - next_addrs[class] = next_addr + slot_size; - break :a next_addr; - } - }; - return @ptrFromInt(addr); - } - const bigpages_needed = bigPagesNeeded(actual_len); - return @ptrFromInt(allocBigPages(bigpages_needed)); -} - -fn resize( - ctx: *anyopaque, - buf: []u8, - alignment: mem.Alignment, - new_len: usize, - return_address: usize, -) bool { - _ = ctx; - _ = return_address; - // We don't want to move anything from one size class to another, but we - // can recover bytes in between powers of two. - const buf_align = alignment.toByteUnits(); - const old_actual_len = @max(buf.len + @sizeOf(usize), buf_align); - const new_actual_len = @max(new_len +| @sizeOf(usize), buf_align); - const old_small_slot_size = math.ceilPowerOfTwoAssert(usize, old_actual_len); - const old_small_class = math.log2(old_small_slot_size) - min_class; - if (old_small_class < size_class_count) { - const new_small_slot_size = math.ceilPowerOfTwo(usize, new_actual_len) catch return false; - return old_small_slot_size == new_small_slot_size; - } else { - const old_bigpages_needed = bigPagesNeeded(old_actual_len); - const old_big_slot_pages = math.ceilPowerOfTwoAssert(usize, old_bigpages_needed); - const new_bigpages_needed = bigPagesNeeded(new_actual_len); - const new_big_slot_pages = math.ceilPowerOfTwo(usize, new_bigpages_needed) catch return false; - return old_big_slot_pages == new_big_slot_pages; - } -} - -fn remap( - context: *anyopaque, - memory: []u8, - alignment: mem.Alignment, - new_len: usize, - return_address: usize, -) ?[*]u8 { - return if (resize(context, memory, alignment, new_len, return_address)) memory.ptr else null; -} - -fn free( - ctx: *anyopaque, - buf: []u8, - alignment: mem.Alignment, - return_address: usize, -) void { - _ = ctx; - _ = return_address; - const buf_align = alignment.toByteUnits(); - const actual_len = @max(buf.len + @sizeOf(usize), buf_align); - const slot_size = math.ceilPowerOfTwoAssert(usize, actual_len); - const class = math.log2(slot_size) - min_class; - const addr = @intFromPtr(buf.ptr); - if (class < size_class_count) { - const node: *usize = @ptrFromInt(addr + (slot_size - @sizeOf(usize))); - node.* = frees[class]; - frees[class] = addr; - } else { - const bigpages_needed = bigPagesNeeded(actual_len); - const pow2_pages = math.ceilPowerOfTwoAssert(usize, bigpages_needed); - const big_slot_size_bytes = pow2_pages * bigpage_size; - const node: *usize = @ptrFromInt(addr + (big_slot_size_bytes - @sizeOf(usize))); - const big_class = math.log2(pow2_pages); - node.* = big_frees[big_class]; - big_frees[big_class] = addr; - } -} - -inline fn bigPagesNeeded(byte_count: usize) usize { - return (byte_count + (bigpage_size + (@sizeOf(usize) - 1))) / bigpage_size; -} - -fn allocBigPages(n: usize) usize { - const pow2_pages = math.ceilPowerOfTwoAssert(usize, n); - const slot_size_bytes = pow2_pages * bigpage_size; - const class = math.log2(pow2_pages); - - const top_free_ptr = big_frees[class]; - if (top_free_ptr != 0) { - const node: *usize = @ptrFromInt(top_free_ptr + (slot_size_bytes - @sizeOf(usize))); - big_frees[class] = node.*; - return top_free_ptr; - } - - const page_index = @wasmMemoryGrow(0, pow2_pages * pages_per_bigpage); - if (page_index == -1) return 0; - return @as(usize, @intCast(page_index)) * wasm.page_size; -} - -const test_ally: Allocator = .{ - .ptr = undefined, - .vtable = &vtable, -}; - -test "small allocations - free in same order" { - var list: [513]*u64 = undefined; - - var i: usize = 0; - while (i < 513) : (i += 1) { - const ptr = try test_ally.create(u64); - list[i] = ptr; - } - - for (list) |ptr| { - test_ally.destroy(ptr); - } -} - -test "small allocations - free in reverse order" { - var list: [513]*u64 = undefined; - - var i: usize = 0; - while (i < 513) : (i += 1) { - const ptr = try test_ally.create(u64); - list[i] = ptr; - } - - i = list.len; - while (i > 0) { - i -= 1; - const ptr = list[i]; - test_ally.destroy(ptr); - } -} - -test "large allocations" { - const ptr1 = try test_ally.alloc(u64, 42768); - const ptr2 = try test_ally.alloc(u64, 52768); - test_ally.free(ptr1); - const ptr3 = try test_ally.alloc(u64, 62768); - test_ally.free(ptr3); - test_ally.free(ptr2); -} - -test "very large allocation" { - try std.testing.expectError(error.OutOfMemory, test_ally.alloc(u8, math.maxInt(usize))); -} - -test "realloc" { - var slice = try test_ally.alignedAlloc(u8, .of(u32), 1); - defer test_ally.free(slice); - slice[0] = 0x12; - - // This reallocation should keep its pointer address. - const old_slice = slice; - slice = try test_ally.realloc(slice, 2); - try std.testing.expect(old_slice.ptr == slice.ptr); - try std.testing.expect(slice[0] == 0x12); - slice[1] = 0x34; - - // This requires upgrading to a larger size class - slice = try test_ally.realloc(slice, 17); - try std.testing.expect(slice[0] == 0x12); - try std.testing.expect(slice[1] == 0x34); -} - -test "shrink" { - var slice = try test_ally.alloc(u8, 20); - defer test_ally.free(slice); - - @memset(slice, 0x11); - - try std.testing.expect(test_ally.resize(slice, 17)); - slice = slice[0..17]; - - for (slice) |b| { - try std.testing.expect(b == 0x11); - } - - try std.testing.expect(test_ally.resize(slice, 16)); - slice = slice[0..16]; - - for (slice) |b| { - try std.testing.expect(b == 0x11); - } -} - -test "large object - grow" { - var slice1 = try test_ally.alloc(u8, bigpage_size * 2 - 20); - defer test_ally.free(slice1); - - const old = slice1; - slice1 = try test_ally.realloc(slice1, bigpage_size * 2 - 10); - try std.testing.expect(slice1.ptr == old.ptr); - - slice1 = try test_ally.realloc(slice1, bigpage_size * 2); - slice1 = try test_ally.realloc(slice1, bigpage_size * 2 + 1); -} - -test "realloc small object to large object" { - var slice = try test_ally.alloc(u8, 70); - defer test_ally.free(slice); - slice[0] = 0x12; - slice[60] = 0x34; - - // This requires upgrading to a large object - const large_object_size = bigpage_size * 2 + 50; - slice = try test_ally.realloc(slice, large_object_size); - try std.testing.expect(slice[0] == 0x12); - try std.testing.expect(slice[60] == 0x34); -} - -test "shrink large object to large object" { - var slice = try test_ally.alloc(u8, bigpage_size * 2 + 50); - defer test_ally.free(slice); - slice[0] = 0x12; - slice[60] = 0x34; - - try std.testing.expect(test_ally.resize(slice, bigpage_size * 2 + 1)); - slice = slice[0 .. bigpage_size * 2 + 1]; - try std.testing.expect(slice[0] == 0x12); - try std.testing.expect(slice[60] == 0x34); - - try std.testing.expect(test_ally.resize(slice, bigpage_size * 2 + 1)); - try std.testing.expect(slice[0] == 0x12); - try std.testing.expect(slice[60] == 0x34); - - slice = try test_ally.realloc(slice, bigpage_size * 2); - try std.testing.expect(slice[0] == 0x12); - try std.testing.expect(slice[60] == 0x34); -} - -test "realloc large object to small object" { - var slice = try test_ally.alloc(u8, bigpage_size * 2 + 50); - defer test_ally.free(slice); - slice[0] = 0x12; - slice[16] = 0x34; - - slice = try test_ally.realloc(slice, 19); - try std.testing.expect(slice[0] == 0x12); - try std.testing.expect(slice[16] == 0x34); -} - -test "objects of size 1024 and 2048" { - const slice = try test_ally.alloc(u8, 1025); - const slice2 = try test_ally.alloc(u8, 3000); - - test_ally.free(slice); - test_ally.free(slice2); -} - -test "standard allocator tests" { - try std.heap.testAllocator(test_ally); - try std.heap.testAllocatorAligned(test_ally); -} diff --git a/lib/std/heap/sbrk_allocator.zig b/lib/std/heap/sbrk_allocator.zig @@ -1,180 +0,0 @@ -const builtin = @import("builtin"); - -const std = @import("../std.zig"); -const Io = std.Io; -const math = std.math; -const Allocator = std.mem.Allocator; -const mem = std.mem; -const heap = std.heap; -const assert = std.debug.assert; - -pub fn SbrkAllocator(comptime sbrk: *const fn (n: usize) usize) type { - return struct { - pub const vtable: Allocator.VTable = .{ - .alloc = alloc, - .resize = resize, - .remap = remap, - .free = free, - }; - - pub const Error = Allocator.Error; - - const max_usize = math.maxInt(usize); - const ushift = math.Log2Int(usize); - const bigpage_size = 64 * 1024; - const pages_per_bigpage = bigpage_size / heap.pageSize(); - const bigpage_count = max_usize / bigpage_size; - - /// Because of storing free list pointers, the minimum size class is 3. - const min_class = math.log2(math.ceilPowerOfTwoAssert(usize, 1 + @sizeOf(usize))); - const size_class_count = math.log2(bigpage_size) - min_class; - /// 0 - 1 bigpage - /// 1 - 2 bigpages - /// 2 - 4 bigpages - /// etc. - const big_size_class_count = math.log2(bigpage_count); - - var next_addrs = [1]usize{0} ** size_class_count; - /// For each size class, points to the freed pointer. - var frees = [1]usize{0} ** size_class_count; - /// For each big size class, points to the freed pointer. - var big_frees = [1]usize{0} ** big_size_class_count; - - // TODO don't do the naive locking strategy - var mutex: Io.Mutex = .{}; - fn alloc(ctx: *anyopaque, len: usize, alignment: mem.Alignment, return_address: usize) ?[*]u8 { - _ = ctx; - _ = return_address; - Io.Threaded.mutexLock(&mutex); - defer Io.Threaded.mutexUnlock(&mutex); - // Make room for the freelist next pointer. - const actual_len = @max(len +| @sizeOf(usize), alignment.toByteUnits()); - const slot_size = math.ceilPowerOfTwo(usize, actual_len) catch return null; - const class = math.log2(slot_size) - min_class; - if (class < size_class_count) { - const addr = a: { - const top_free_ptr = frees[class]; - if (top_free_ptr != 0) { - const node = @as(*usize, @ptrFromInt(top_free_ptr + (slot_size - @sizeOf(usize)))); - frees[class] = node.*; - break :a top_free_ptr; - } - - const next_addr = next_addrs[class]; - if (next_addr % heap.pageSize() == 0) { - const addr = allocBigPages(1); - if (addr == 0) return null; - //std.debug.print("allocated fresh slot_size={d} class={d} addr=0x{x}\n", .{ - // slot_size, class, addr, - //}); - next_addrs[class] = addr + slot_size; - break :a addr; - } else { - next_addrs[class] = next_addr + slot_size; - break :a next_addr; - } - }; - return @as([*]u8, @ptrFromInt(addr)); - } - const bigpages_needed = bigPagesNeeded(actual_len); - const addr = allocBigPages(bigpages_needed); - return @as([*]u8, @ptrFromInt(addr)); - } - - fn resize( - ctx: *anyopaque, - buf: []u8, - alignment: mem.Alignment, - new_len: usize, - return_address: usize, - ) bool { - _ = ctx; - _ = return_address; - Io.Threaded.mutexLock(&mutex); - defer Io.Threaded.mutexUnlock(&mutex); - // We don't want to move anything from one size class to another, but we - // can recover bytes in between powers of two. - const buf_align = alignment.toByteUnits(); - const old_actual_len = @max(buf.len + @sizeOf(usize), buf_align); - const new_actual_len = @max(new_len +| @sizeOf(usize), buf_align); - const old_small_slot_size = math.ceilPowerOfTwoAssert(usize, old_actual_len); - const old_small_class = math.log2(old_small_slot_size) - min_class; - if (old_small_class < size_class_count) { - const new_small_slot_size = math.ceilPowerOfTwo(usize, new_actual_len) catch return false; - return old_small_slot_size == new_small_slot_size; - } else { - const old_bigpages_needed = bigPagesNeeded(old_actual_len); - const old_big_slot_pages = math.ceilPowerOfTwoAssert(usize, old_bigpages_needed); - const new_bigpages_needed = bigPagesNeeded(new_actual_len); - const new_big_slot_pages = math.ceilPowerOfTwo(usize, new_bigpages_needed) catch return false; - return old_big_slot_pages == new_big_slot_pages; - } - } - - fn remap( - context: *anyopaque, - memory: []u8, - alignment: mem.Alignment, - new_len: usize, - return_address: usize, - ) ?[*]u8 { - return if (resize(context, memory, alignment, new_len, return_address)) memory.ptr else null; - } - - fn free( - ctx: *anyopaque, - buf: []u8, - alignment: mem.Alignment, - return_address: usize, - ) void { - _ = ctx; - _ = return_address; - Io.Threaded.mutexLock(&mutex); - defer Io.Threaded.mutexUnlock(&mutex); - const buf_align = alignment.toByteUnits(); - const actual_len = @max(buf.len + @sizeOf(usize), buf_align); - const slot_size = math.ceilPowerOfTwoAssert(usize, actual_len); - const class = math.log2(slot_size) - min_class; - const addr = @intFromPtr(buf.ptr); - if (class < size_class_count) { - const node = @as(*usize, @ptrFromInt(addr + (slot_size - @sizeOf(usize)))); - node.* = frees[class]; - frees[class] = addr; - } else { - const bigpages_needed = bigPagesNeeded(actual_len); - const pow2_pages = math.ceilPowerOfTwoAssert(usize, bigpages_needed); - const big_slot_size_bytes = pow2_pages * bigpage_size; - const node = @as(*usize, @ptrFromInt(addr + (big_slot_size_bytes - @sizeOf(usize)))); - const big_class = math.log2(pow2_pages); - node.* = big_frees[big_class]; - big_frees[big_class] = addr; - } - } - - inline fn bigPagesNeeded(byte_count: usize) usize { - return (byte_count + (bigpage_size + (@sizeOf(usize) - 1))) / bigpage_size; - } - - fn allocBigPages(n: usize) usize { - const pow2_pages = math.ceilPowerOfTwoAssert(usize, n); - const slot_size_bytes = pow2_pages * bigpage_size; - const class = math.log2(pow2_pages); - - const top_free_ptr = big_frees[class]; - if (top_free_ptr != 0) { - const node = @as(*usize, @ptrFromInt(top_free_ptr + (slot_size_bytes - @sizeOf(usize)))); - big_frees[class] = node.*; - return top_free_ptr; - } - return sbrk(pow2_pages * pages_per_bigpage * heap.pageSize()); - } - }; -} - -test SbrkAllocator { - _ = SbrkAllocator(struct { - fn sbrk(_: usize) usize { - return 0; - } - }.sbrk); -} diff --git a/lib/std/os/linux.zig b/lib/std/os/linux.zig @@ -594,6 +594,10 @@ pub fn errno(r: usize) E { return @enumFromInt(int); } +pub fn brk(addr: usize) usize { + return syscall1(.brk, addr); +} + pub fn dup(old: i32) usize { return syscall1(.dup, @as(usize, @bitCast(@as(isize, old)))); }