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 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 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, .{}); } 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 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. pub const item_size = @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 = @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 = @as(NodePtr, @ptrCast(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) @as(NodePtr, @ptrCast(try pool.allocNew())) else return error.OutOfMemory; const ptr = @as(ItemPtr, @ptrCast(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 = @as(NodePtr, @ptrCast(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 reuse 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()); }