From e28f4a1d859bbfe2f08074159a05e03d30d2cf81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Tue, 23 Aug 2022 11:12:24 +0200 Subject: [PATCH 1/2] Implements std.heap.MemoryPool and friends. --- lib/std/heap.zig | 8 ++ lib/std/heap/memory_pool.zig | 183 +++++++++++++++++++++++++++++++++++ 2 files changed, 191 insertions(+) create mode 100644 lib/std/heap/memory_pool.zig diff --git a/lib/std/heap.zig b/lib/std/heap.zig index ee6525659e..e6c17aeebb 100644 --- a/lib/std/heap.zig +++ b/lib/std/heap.zig @@ -20,9 +20,16 @@ pub const WasmAllocator = @import("heap/WasmAllocator.zig"); pub const WasmPageAllocator = @import("heap/WasmPageAllocator.zig"); pub const PageAllocator = @import("heap/PageAllocator.zig"); +const memory_pool = @import("heap/memory_pool.zig"); +pub const MemoryPool = memory_pool.MemoryPool; +pub const MemoryPoolAligned = memory_pool.MemoryPoolAligned; +pub const MemoryPoolExtra = memory_pool.MemoryPoolExtra; +pub const MemoryPoolOptions = memory_pool.Options; + /// TODO Utilize this on Windows. pub var next_mmap_addr_hint: ?[*]align(mem.page_size) u8 = null; + const CAllocator = struct { comptime { if (!builtin.link_libc) { @@ -847,6 +854,7 @@ test { _ = LoggingAllocator; _ = LogToWriterAllocator; _ = ScopedLoggingAllocator; + _ = @import("heap/memory_pool.zig"); _ = ArenaAllocator; _ = GeneralPurposeAllocator; if (comptime builtin.target.isWasm()) { diff --git a/lib/std/heap/memory_pool.zig b/lib/std/heap/memory_pool.zig new file mode 100644 index 0000000000..e370803e64 --- /dev/null +++ b/lib/std/heap/memory_pool.zig @@ -0,0 +1,183 @@ +const std = @import("../std.zig"); + +const debug_mode = @import("builtin").mode == .Debug; + +pub const MemoryPoolError = error{OutOfMemory}; + +/// A memory pool that can allocate objects of a single type very quickly. +/// Use this instead of a general purpose allocator when you need to allocate a lot of objects +/// of the same type, as a memory pool outperforms general purpose allocators. +pub fn MemoryPool(comptime Item: type) type { + return MemoryPoolAligned(Item, @alignOf(Item)); +} + +/// A memory pool that can allocate objects of a single type very quickly. +/// Use this instead of a general purpose allocator when you need to allocate a lot of objects +/// of the same type, as a memory pool outperforms general purpose allocators. +pub fn MemoryPoolAligned(comptime Item: type, comptime alignment: u29) type { + if (@alignOf(Item) == alignment) { + return MemoryPoolExtra(Item, .{}); + } else { + return MemoryPoolExtra(Item, .{ .alignment = alignment }); + } +} + +pub const Options = struct { + /// The alignment of the memory pool items. Use `null` for natural alignment. + alignment: ?u29 = null, + + /// If `true`, the memory pool can allocate additional items after a initial setup. + /// If `false`, the memory pool will not allocate further after a call to `initPreheated`. + growable: bool = true, +}; + +/// A memory pool that can allocate objects of a single type very quickly. +/// Use this instead of a general purpose allocator when you need to allocate a lot of objects +/// of the same type, as a memory pool outperforms general purpose allocators. +pub fn MemoryPoolExtra(comptime Item: type, comptime pool_options: Options) type { + return struct { + const Pool = @This(); + + /// Size of the memory pool items. This is not necessarily + /// the same as `@sizeOf(Item)` as the pool also uses the items for internal + /// means. + pub const item_size = std.math.max(@sizeOf(Node), @sizeOf(Item)); + + /// Alignment of the memory pool items. This is not necessarily + /// the same as `@alignOf(Item)` as the pool also uses the items for internal + /// means. + pub const item_alignment = std.math.max(@alignOf(Node), pool_options.alignment orelse 0); + + const Node = struct { + next: ?*@This(), + }; + const NodePtr = *align(item_alignment) Node; + const ItemPtr = *align(item_alignment) Item; + + arena: std.heap.ArenaAllocator, + free_list: ?NodePtr = null, + + /// Creates a new memory pool. + pub fn init(allocator: std.mem.Allocator) Pool { + return .{ .arena = std.heap.ArenaAllocator.init(allocator) }; + } + + /// Creates a new memory pool and pre-allocates `initial_size` items. + /// This allows the up to `initial_size` active allocations before a + /// `OutOfMemory` error happens when calling `create()`. + pub fn initPreheated(allocator: std.mem.Allocator, initial_size: usize) MemoryPoolError!Pool { + var pool = init(allocator); + errdefer pool.deinit(); + + var i: usize = 0; + while (i < initial_size) : (i += 1) { + const raw_mem = try pool.allocNew(); + const free_node = @ptrCast(NodePtr, raw_mem); + free_node.* = Node{ + .next = pool.free_list, + }; + pool.free_list = free_node; + } + + return pool; + } + + /// Destroys the memory pool and frees all allocated memory. + pub fn deinit(pool: *Pool) void { + pool.arena.deinit(); + pool.* = undefined; + } + + /// Resets the memory pool and destroys all allocated items. + /// This can be used to batch-destroy all objects without invalidating the memory pool. + pub fn reset(pool: *Pool) void { + // TODO: Potentially store all allocated objects in a list as well, allowing to + // just move them into the free list instead of actually releasing the memory. + const allocator = pool.arena.child_allocator; + + // TODO: Replace with "pool.arena.reset()" when implemented. + pool.arena.deinit(); + pool.arena = std.heap.ArenaAllocator.init(allocator); + + pool.free_list = null; + } + + /// Creates a new item and adds it to the memory pool. + pub fn create(pool: *Pool) !ItemPtr { + const node = if (pool.free_list) |item| blk: { + pool.free_list = item.next; + break :blk item; + } else if (pool_options.growable) + @ptrCast(NodePtr, try pool.allocNew()) + else + return error.OutOfMemory; + + const ptr = @ptrCast(ItemPtr, node); + ptr.* = undefined; + return ptr; + } + + /// Destroys a previously created item. + /// Only pass items to `ptr` that were previously created with `create()` of the same memory pool! + pub fn destroy(pool: *Pool, ptr: ItemPtr) void { + ptr.* = undefined; + + const node = @ptrCast(NodePtr, ptr); + node.* = Node{ + .next = pool.free_list, + }; + pool.free_list = node; + } + + fn allocNew(pool: *Pool) MemoryPoolError!*align(item_alignment) [item_size]u8 { + const mem = try pool.arena.allocator().alignedAlloc(u8, item_alignment, item_size); + return mem[0..item_size]; // coerce slice to array pointer + } + }; +} + +test "memory pool: basic" { + var pool = MemoryPool(u32).init(std.testing.allocator); + defer pool.deinit(); + + const p1 = try pool.create(); + const p2 = try pool.create(); + const p3 = try pool.create(); + + // Assert uniqueness + try std.testing.expect(p1 != p2); + try std.testing.expect(p1 != p3); + try std.testing.expect(p2 != p3); + + pool.destroy(p2); + const p4 = try pool.create(); + + // Assert memory resuse + try std.testing.expect(p2 == p4); +} + +test "memory pool: preheating (success)" { + var pool = try MemoryPool(u32).initPreheated(std.testing.allocator, 4); + defer pool.deinit(); + + _ = try pool.create(); + _ = try pool.create(); + _ = try pool.create(); +} + +test "memory pool: preheating (failure)" { + var failer = std.testing.FailingAllocator.init(std.testing.allocator, 0); + try std.testing.expectError(error.OutOfMemory, MemoryPool(u32).initPreheated(failer.allocator(), 5)); +} + +test "memory pool: growable" { + var pool = try MemoryPoolExtra(u32, .{ .growable = false }).initPreheated(std.testing.allocator, 4); + defer pool.deinit(); + + _ = try pool.create(); + _ = try pool.create(); + _ = try pool.create(); + _ = try pool.create(); + + try std.testing.expectError(error.OutOfMemory, pool.create()); +} From 108b3c5673908057df7b96cec61d9bcadaa9f197 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20=22xq=22=20Quei=C3=9Fner?= Date: Tue, 23 Aug 2022 15:25:27 +0200 Subject: [PATCH 2/2] Improves the comment formatting. --- lib/std/heap.zig | 1 - lib/std/heap/memory_pool.zig | 22 ++++++++++------------ 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/lib/std/heap.zig b/lib/std/heap.zig index e6c17aeebb..b662cd39bd 100644 --- a/lib/std/heap.zig +++ b/lib/std/heap.zig @@ -29,7 +29,6 @@ pub const MemoryPoolOptions = memory_pool.Options; /// TODO Utilize this on Windows. pub var next_mmap_addr_hint: ?[*]align(mem.page_size) u8 = null; - const CAllocator = struct { comptime { if (!builtin.link_libc) { diff --git a/lib/std/heap/memory_pool.zig b/lib/std/heap/memory_pool.zig index e370803e64..6ae90a3894 100644 --- a/lib/std/heap/memory_pool.zig +++ b/lib/std/heap/memory_pool.zig @@ -5,15 +5,15 @@ const debug_mode = @import("builtin").mode == .Debug; pub const MemoryPoolError = error{OutOfMemory}; /// A memory pool that can allocate objects of a single type very quickly. -/// Use this instead of a general purpose allocator when you need to allocate a lot of objects -/// of the same type, as a memory pool outperforms general purpose allocators. +/// Use this when you need to allocate a lot of objects of the same type, +/// because It outperforms general purpose allocators. pub fn MemoryPool(comptime Item: type) type { return MemoryPoolAligned(Item, @alignOf(Item)); } /// A memory pool that can allocate objects of a single type very quickly. -/// Use this instead of a general purpose allocator when you need to allocate a lot of objects -/// of the same type, as a memory pool outperforms general purpose allocators. +/// Use this when you need to allocate a lot of objects of the same type, +/// because It outperforms general purpose allocators. pub fn MemoryPoolAligned(comptime Item: type, comptime alignment: u29) type { if (@alignOf(Item) == alignment) { return MemoryPoolExtra(Item, .{}); @@ -32,20 +32,18 @@ pub const Options = struct { }; /// A memory pool that can allocate objects of a single type very quickly. -/// Use this instead of a general purpose allocator when you need to allocate a lot of objects -/// of the same type, as a memory pool outperforms general purpose allocators. +/// Use this when you need to allocate a lot of objects of the same type, +/// because It outperforms general purpose allocators. pub fn MemoryPoolExtra(comptime Item: type, comptime pool_options: Options) type { return struct { const Pool = @This(); - /// Size of the memory pool items. This is not necessarily - /// the same as `@sizeOf(Item)` as the pool also uses the items for internal - /// means. + /// Size of the memory pool items. This is not necessarily the same + /// as `@sizeOf(Item)` as the pool also uses the items for internal means. pub const item_size = std.math.max(@sizeOf(Node), @sizeOf(Item)); - /// Alignment of the memory pool items. This is not necessarily - /// the same as `@alignOf(Item)` as the pool also uses the items for internal - /// means. + /// Alignment of the memory pool items. This is not necessarily the same + /// as `@alignOf(Item)` as the pool also uses the items for internal means. pub const item_alignment = std.math.max(@alignOf(Node), pool_options.alignment orelse 0); const Node = struct {