Commit 4187d0e8fe

Justus Klausecker <justus@klausecker.de>
2025-08-13 19:13:43
MemoryPool: add unmanaged variants and make them the default
1 parent 2508036
Changed files (3)
lib
compiler
std
lib/compiler/resinator/source_mapping.zig
@@ -723,7 +723,7 @@ pub const SourceMappings = struct {
     /// The default assumes that the first filename added is the root file.
     /// The value should be set to the correct offset if that assumption does not hold.
     root_filename_offset: u32 = 0,
-    source_node_pool: std.heap.MemoryPool(Sources.Node) = std.heap.MemoryPool(Sources.Node).init(std.heap.page_allocator),
+    source_node_pool: std.heap.MemoryPool(Sources.Node) = .empty,
     end_line: usize = 0,
 
     const sourceCompare = struct {
@@ -742,7 +742,7 @@ pub const SourceMappings = struct {
 
     pub fn deinit(self: *SourceMappings, allocator: Allocator) void {
         self.files.deinit(allocator);
-        self.source_node_pool.deinit();
+        self.source_node_pool.deinit(std.heap.page_allocator);
     }
 
     /// Find the node that 'contains' the `line`, i.e. the node's start_line is
@@ -823,7 +823,7 @@ pub const SourceMappings = struct {
                 .filename_offset = filename_offset,
             };
             var entry = self.sources.getEntryFor(key);
-            var new_node = try self.source_node_pool.create();
+            var new_node = try self.source_node_pool.create(std.heap.page_allocator);
             new_node.key = key;
             entry.set(new_node);
         }
@@ -869,7 +869,7 @@ pub const SourceMappings = struct {
                 .filename_offset = node.key.filename_offset,
             };
             var entry = self.sources.getEntryFor(key);
-            var new_node = try self.source_node_pool.create();
+            var new_node = try self.source_node_pool.create(std.heap.page_allocator);
             new_node.key = key;
             entry.set(new_node);
             node = new_node;
lib/std/heap/memory_pool.zig
@@ -1,26 +1,26 @@
 const std = @import("../std.zig");
+const Allocator = std.mem.Allocator;
 const Alignment = std.mem.Alignment;
+const MemoryPool = std.heap.MemoryPool;
 
-const debug_mode = @import("builtin").mode == .Debug;
-
-pub const MemoryPoolError = error{OutOfMemory};
+/// Deprecated.
+pub fn Managed(comptime Item: type) type {
+    return ExtraManaged(Item, .{ .alignment = null });
+}
 
 /// 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, .of(Item));
+/// because it outperforms general purpose allocators.
+/// Allocated items are aligned to `alignment`-byte addresses or `@alignOf(Item)`
+/// if `alignment` is `null`.
+/// Functions that potentially allocate memory accept an `Allocator` parameter.
+pub fn Aligned(comptime Item: type, comptime alignment: Alignment) type {
+    return Extra(Item, .{ .alignment = alignment });
 }
 
-/// 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: Alignment) type {
-    if (@alignOf(Item) == comptime alignment.toByteUnits()) {
-        return MemoryPoolExtra(Item, .{});
-    } else {
-        return MemoryPoolExtra(Item, .{ .alignment = alignment });
-    }
+/// Deprecated.
+pub fn AlignedManaged(comptime Item: type, comptime alignment: Alignment) type {
+    return ExtraManaged(Item, .{ .alignment = alignment });
 }
 
 pub const Options = struct {
@@ -34,64 +34,70 @@ pub const Options = struct {
 
 /// 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 {
+/// because it outperforms general purpose allocators.
+/// Functions that potentially allocate memory accept an `Allocator` parameter.
+pub fn Extra(comptime Item: type, comptime pool_options: Options) type {
+    if (pool_options.alignment) |a| {
+        if (a.compare(.eq, .of(Item))) {
+            var new_options = pool_options;
+            new_options.alignment = null;
+            return Extra(Item, new_options);
+        }
+    }
     return struct {
         const Pool = @This();
 
+        arena_state: std.heap.ArenaAllocator.State,
+        free_list: std.SinglyLinkedList,
+
         /// 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));
 
-        // This needs to be kept in sync with Node.
-        const node_alignment: Alignment = .of(*anyopaque);
-
         /// 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: Alignment = node_alignment.max(pool_options.alignment orelse .of(Item));
+        pub const item_alignment: Alignment = .max(pool_options.alignment orelse .of(Item), .of(Node));
 
-        const Node = struct {
-            next: ?*align(item_alignment.toByteUnits()) @This(),
-        };
-        const NodePtr = *align(item_alignment.toByteUnits()) Node;
+        const Node = std.SinglyLinkedList.Node;
         const ItemPtr = *align(item_alignment.toByteUnits()) 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) };
-        }
+        /// A MemoryPool containing no elements.
+        pub const empty: Pool = .{
+            .arena_state = .{},
+            .free_list = .{},
+        };
 
-        /// 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();
-            try pool.preheat(initial_size);
+        /// Creates a new memory pool and pre-allocates `num` items.
+        /// This allows up to `num` active allocations before an
+        /// `OutOfMemory` error might happen when calling `create()`.
+        pub fn initCapacity(allocator: Allocator, num: usize) Allocator.Error!Pool {
+            var pool: Pool = .empty;
+            errdefer pool.deinit(allocator);
+            try pool.addCapacity(allocator, num);
             return pool;
         }
 
         /// Destroys the memory pool and frees all allocated memory.
-        pub fn deinit(pool: *Pool) void {
-            pool.arena.deinit();
+        pub fn deinit(pool: *Pool, allocator: Allocator) void {
+            pool.arena_state.promote(allocator).deinit();
             pool.* = undefined;
         }
 
-        /// Preheats the memory pool by pre-allocating `size` items.
-        /// This allows up to `size` active allocations before an
+        pub fn toManaged(pool: Pool, allocator: Allocator) ExtraManaged(Item, pool_options) {
+            return .{
+                .allocator = allocator,
+                .unmanaged = pool,
+            };
+        }
+
+        /// Pre-allocates `num` items and adds them to the memory pool.
+        /// This allows at least `num` active allocations before an
         /// `OutOfMemory` error might happen when calling `create()`.
-        pub fn preheat(pool: *Pool, size: usize) MemoryPoolError!void {
+        pub fn addCapacity(pool: *Pool, allocator: Allocator, num: usize) Allocator.Error!void {
             var i: usize = 0;
-            while (i < 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;
+            while (i < num) : (i += 1) {
+                const memory = try pool.allocNew(allocator);
+                pool.free_list.prepend(@ptrCast(memory));
             }
         }
 
@@ -106,28 +112,29 @@ pub fn MemoryPoolExtra(comptime Item: type, comptime pool_options: Options) type
         /// be slower.
         ///
         /// NOTE: If `mode` is `free_all`, the function will always return `true`.
-        pub fn reset(pool: *Pool, mode: ResetMode) bool {
+        pub fn reset(pool: *Pool, allocator: Allocator, mode: ResetMode) bool {
             // 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 reset_successful = pool.arena.reset(mode);
+            var arena = pool.arena_state.promote(allocator);
+            defer pool.arena_state = arena.state;
 
-            pool.free_list = null;
+            const reset_successful = arena.reset(mode);
+            pool.free_list = .{};
 
             return reset_successful;
         }
 
         /// 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()))
+        /// `allocator` may be `undefined` if pool is not `growable`.
+        pub fn create(pool: *Pool, allocator: Allocator) Allocator.Error!ItemPtr {
+            const ptr: ItemPtr = if (pool.free_list.popFirst()) |node|
+                @ptrCast(@alignCast(node))
+            else if (pool_options.growable)
+                @ptrCast(try pool.allocNew(allocator))
             else
                 return error.OutOfMemory;
 
-            const ptr = @as(ItemPtr, @ptrCast(node));
             ptr.* = undefined;
             return ptr;
         }
@@ -136,87 +143,238 @@ pub fn MemoryPoolExtra(comptime Item: type, comptime pool_options: Options) type
         /// 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;
+            pool.free_list.prepend(@ptrCast(ptr));
+        }
 
-            const node = @as(NodePtr, @ptrCast(ptr));
-            node.* = Node{
-                .next = pool.free_list,
-            };
-            pool.free_list = node;
+        fn allocNew(pool: *Pool, allocator: Allocator) Allocator.Error!*align(item_alignment.toByteUnits()) [item_size]u8 {
+            var arena = pool.arena_state.promote(allocator);
+            defer pool.arena_state = arena.state;
+            const memory = try arena.allocator().alignedAlloc(u8, item_alignment, item_size);
+            return memory[0..item_size];
+        }
+    };
+}
+
+/// Deprecated.
+pub fn ExtraManaged(comptime Item: type, comptime pool_options: Options) type {
+    if (pool_options.alignment) |a| {
+        if (a.compare(.eq, .of(Item))) {
+            var new_options = pool_options;
+            new_options.alignment = null;
+            return ExtraManaged(Item, new_options);
+        }
+    }
+    return struct {
+        const Pool = @This();
+
+        allocator: Allocator,
+        unmanaged: Unmanaged,
+
+        pub const Unmanaged = Extra(Item, pool_options);
+        pub const item_size = Unmanaged.item_size;
+        pub const item_alignment = Unmanaged.item_alignment;
+
+        const ItemPtr = Unmanaged.ItemPtr;
+
+        /// Creates a new memory pool.
+        pub fn init(allocator: Allocator) Pool {
+            return Unmanaged.empty.toManaged(allocator);
+        }
+
+        /// Creates a new memory pool and pre-allocates `num` items.
+        /// This allows up to `num` active allocations before an
+        /// `OutOfMemory` error might happen when calling `create()`.
+        pub fn initCapacity(allocator: Allocator, num: usize) Allocator.Error!Pool {
+            return (try Unmanaged.initCapacity(allocator, num)).toManaged(allocator);
+        }
+
+        /// Destroys the memory pool and frees all allocated memory.
+        pub fn deinit(pool: *Pool) void {
+            pool.unmanaged.deinit(pool.allocator);
+            pool.* = undefined;
+        }
+
+        /// Pre-allocates `num` items and adds them to the memory pool.
+        /// This allows at least `num` active allocations before an
+        /// `OutOfMemory` error might happen when calling `create()`.
+        pub fn addCapacity(pool: *Pool, num: usize) Allocator.Error!void {
+            return pool.unmanaged.addCapacity(pool.allocator, num);
+        }
+
+        pub const ResetMode = Unmanaged.ResetMode;
+
+        /// Resets the memory pool and destroys all allocated items.
+        /// This can be used to batch-destroy all objects without invalidating the memory pool.
+        ///
+        /// The function will return whether the reset operation was successful or not.
+        /// If the reallocation  failed `false` is returned. The pool will still be fully
+        /// functional in that case, all memory is released. Future allocations just might
+        /// be slower.
+        ///
+        /// NOTE: If `mode` is `free_all`, the function will always return `true`.
+        pub fn reset(pool: *Pool, mode: ResetMode) bool {
+            return pool.unmanaged.reset(pool.allocator, mode);
         }
 
-        fn allocNew(pool: *Pool) MemoryPoolError!*align(item_alignment.toByteUnits()) [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
+        /// Creates a new item and adds it to the memory pool.
+        pub fn create(pool: *Pool) Allocator.Error!ItemPtr {
+            return pool.unmanaged.create(pool.allocator);
+        }
+
+        /// 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 {
+            return pool.unmanaged.destroy(ptr);
+        }
+
+        fn allocNew(pool: *Pool) Allocator.Error!*align(item_alignment) [item_size]u8 {
+            return pool.unmanaged.allocNew(pool.allocator);
         }
     };
 }
 
 test "basic" {
-    var pool = MemoryPool(u32).init(std.testing.allocator);
-    defer pool.deinit();
+    const a = std.testing.allocator;
+
+    {
+        var pool: MemoryPool(u32) = .empty;
+        defer pool.deinit(a);
 
-    const p1 = try pool.create();
-    const p2 = try pool.create();
-    const p3 = try pool.create();
+        const p1 = try pool.create(a);
+        const p2 = try pool.create(a);
+        const p3 = try pool.create(a);
 
-    // Assert uniqueness
-    try std.testing.expect(p1 != p2);
-    try std.testing.expect(p1 != p3);
-    try std.testing.expect(p2 != p3);
+        // 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(a);
+
+        // Assert memory reuse
+        try std.testing.expect(p2 == p4);
+    }
 
-    pool.destroy(p2);
-    const p4 = try pool.create();
+    {
+        var pool: Managed(u32) = .init(std.testing.allocator);
+        defer pool.deinit();
 
-    // Assert memory reuse
-    try std.testing.expect(p2 == p4);
+        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 "preheating (success)" {
-    var pool = try MemoryPool(u32).initPreheated(std.testing.allocator, 4);
-    defer pool.deinit();
+test "initCapacity (success)" {
+    const a = std.testing.allocator;
+
+    {
+        var pool: MemoryPool(u32) = try .initCapacity(a, 4);
+        defer pool.deinit(a);
+
+        _ = try pool.create(a);
+        _ = try pool.create(a);
+        _ = try pool.create(a);
+    }
+
+    {
+        var pool: Managed(u32) = try .initCapacity(a, 4);
+        defer pool.deinit();
 
-    _ = try pool.create();
-    _ = try pool.create();
-    _ = try pool.create();
+        _ = try pool.create();
+        _ = try pool.create();
+        _ = try pool.create();
+    }
 }
 
-test "preheating (failure)" {
+test "initCapacity (failure)" {
     const failer = std.testing.failing_allocator;
-    try std.testing.expectError(error.OutOfMemory, MemoryPool(u32).initPreheated(failer, 5));
+    try std.testing.expectError(error.OutOfMemory, MemoryPool(u32).initCapacity(failer, 5));
+    try std.testing.expectError(error.OutOfMemory, Managed(u32).initCapacity(failer, 5));
 }
 
 test "growable" {
-    var pool = try MemoryPoolExtra(u32, .{ .growable = false }).initPreheated(std.testing.allocator, 4);
-    defer pool.deinit();
+    const a = std.testing.allocator;
 
-    _ = try pool.create();
-    _ = try pool.create();
-    _ = try pool.create();
-    _ = try pool.create();
+    {
+        var pool: Extra(u32, .{ .growable = false }) = try .initCapacity(a, 4);
+        defer pool.deinit(a);
+
+        _ = try pool.create(a);
+        _ = try pool.create(a);
+        _ = try pool.create(a);
+        _ = try pool.create(a);
+
+        try std.testing.expectError(error.OutOfMemory, pool.create(a));
+    }
 
-    try std.testing.expectError(error.OutOfMemory, pool.create());
+    {
+        var pool: ExtraManaged(u32, .{ .growable = false }) = try .initCapacity(a, 4);
+        defer pool.deinit();
+
+        _ = try pool.create();
+        _ = try pool.create();
+        _ = try pool.create();
+        _ = try pool.create();
+
+        try std.testing.expectError(error.OutOfMemory, pool.create());
+    }
 }
 
 test "greater than pointer default alignment" {
     const Foo = struct {
         data: u64 align(16),
     };
+    const a = std.testing.allocator;
+
+    {
+        var pool: MemoryPool(Foo) = .empty;
+        defer pool.deinit(a);
+
+        const foo: *Foo = try pool.create(a);
+        pool.destroy(foo);
+    }
 
-    var pool = MemoryPool(Foo).init(std.testing.allocator);
-    defer pool.deinit();
+    {
+        var pool: Managed(Foo) = .init(a);
+        defer pool.deinit();
 
-    const foo: *Foo = try pool.create();
-    _ = foo;
+        const foo: *Foo = try pool.create();
+        pool.destroy(foo);
+    }
 }
 
 test "greater than pointer manual alignment" {
     const Foo = struct {
         data: u64,
     };
+    const a = std.testing.allocator;
+
+    {
+        var pool: Aligned(Foo, .@"16") = .empty;
+        defer pool.deinit(a);
 
-    var pool = MemoryPoolAligned(Foo, .@"16").init(std.testing.allocator);
-    defer pool.deinit();
+        const foo: *align(16) Foo = try pool.create(a);
+        pool.destroy(foo);
+    }
 
-    const foo: *align(16) Foo = try pool.create();
-    _ = foo;
+    {
+        var pool: AlignedManaged(Foo, .@"16") = .init(a);
+        defer pool.deinit();
+
+        const foo: *align(16) Foo = try pool.create();
+        pool.destroy(foo);
+    }
 }
lib/std/heap.zig
@@ -25,10 +25,20 @@ pub const GeneralPurposeAllocatorConfig = DebugAllocatorConfig;
 /// Deprecated; to be removed after 0.14.0 is tagged.
 pub const GeneralPurposeAllocator = DebugAllocator;
 
-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;
+/// 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.
+/// Functions that potentially allocate memory accept an `Allocator` parameter.
+pub fn MemoryPool(comptime Item: type) type {
+    return memory_pool.Extra(Item, .{ .alignment = null });
+}
+pub const memory_pool = @import("heap/memory_pool.zig");
+
+/// Deprecated; use `memory_pool.Aligned`.
+pub const MemoryPoolAligned = memory_pool.Aligned;
+/// Deprecated; use `memory_pool.Extra`.
+pub const MemoryPoolExtra = memory_pool.Extra;
+/// Deprecated; use `memory_pool.Options`.
 pub const MemoryPoolOptions = memory_pool.Options;
 
 /// TODO Utilize this on Windows.