Skip to content

Commit e28f4a1

Browse files
author
Felix "xq" Queißner
committed
Implements std.heap.MemoryPool and friends.
1 parent 83e0e23 commit e28f4a1

2 files changed

Lines changed: 191 additions & 0 deletions

File tree

lib/std/heap.zig

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,16 @@ pub const WasmAllocator = @import("heap/WasmAllocator.zig");
2020
pub const WasmPageAllocator = @import("heap/WasmPageAllocator.zig");
2121
pub const PageAllocator = @import("heap/PageAllocator.zig");
2222

23+
const memory_pool = @import("heap/memory_pool.zig");
24+
pub const MemoryPool = memory_pool.MemoryPool;
25+
pub const MemoryPoolAligned = memory_pool.MemoryPoolAligned;
26+
pub const MemoryPoolExtra = memory_pool.MemoryPoolExtra;
27+
pub const MemoryPoolOptions = memory_pool.Options;
28+
2329
/// TODO Utilize this on Windows.
2430
pub var next_mmap_addr_hint: ?[*]align(mem.page_size) u8 = null;
2531

32+
2633
const CAllocator = struct {
2734
comptime {
2835
if (!builtin.link_libc) {
@@ -847,6 +854,7 @@ test {
847854
_ = LoggingAllocator;
848855
_ = LogToWriterAllocator;
849856
_ = ScopedLoggingAllocator;
857+
_ = @import("heap/memory_pool.zig");
850858
_ = ArenaAllocator;
851859
_ = GeneralPurposeAllocator;
852860
if (comptime builtin.target.isWasm()) {

lib/std/heap/memory_pool.zig

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
const std = @import("../std.zig");
2+
3+
const debug_mode = @import("builtin").mode == .Debug;
4+
5+
pub const MemoryPoolError = error{OutOfMemory};
6+
7+
/// A memory pool that can allocate objects of a single type very quickly.
8+
/// Use this instead of a general purpose allocator when you need to allocate a lot of objects
9+
/// of the same type, as a memory pool outperforms general purpose allocators.
10+
pub fn MemoryPool(comptime Item: type) type {
11+
return MemoryPoolAligned(Item, @alignOf(Item));
12+
}
13+
14+
/// A memory pool that can allocate objects of a single type very quickly.
15+
/// Use this instead of a general purpose allocator when you need to allocate a lot of objects
16+
/// of the same type, as a memory pool outperforms general purpose allocators.
17+
pub fn MemoryPoolAligned(comptime Item: type, comptime alignment: u29) type {
18+
if (@alignOf(Item) == alignment) {
19+
return MemoryPoolExtra(Item, .{});
20+
} else {
21+
return MemoryPoolExtra(Item, .{ .alignment = alignment });
22+
}
23+
}
24+
25+
pub const Options = struct {
26+
/// The alignment of the memory pool items. Use `null` for natural alignment.
27+
alignment: ?u29 = null,
28+
29+
/// If `true`, the memory pool can allocate additional items after a initial setup.
30+
/// If `false`, the memory pool will not allocate further after a call to `initPreheated`.
31+
growable: bool = true,
32+
};
33+
34+
/// A memory pool that can allocate objects of a single type very quickly.
35+
/// Use this instead of a general purpose allocator when you need to allocate a lot of objects
36+
/// of the same type, as a memory pool outperforms general purpose allocators.
37+
pub fn MemoryPoolExtra(comptime Item: type, comptime pool_options: Options) type {
38+
return struct {
39+
const Pool = @This();
40+
41+
/// Size of the memory pool items. This is not necessarily
42+
/// the same as `@sizeOf(Item)` as the pool also uses the items for internal
43+
/// means.
44+
pub const item_size = std.math.max(@sizeOf(Node), @sizeOf(Item));
45+
46+
/// Alignment of the memory pool items. This is not necessarily
47+
/// the same as `@alignOf(Item)` as the pool also uses the items for internal
48+
/// means.
49+
pub const item_alignment = std.math.max(@alignOf(Node), pool_options.alignment orelse 0);
50+
51+
const Node = struct {
52+
next: ?*@This(),
53+
};
54+
const NodePtr = *align(item_alignment) Node;
55+
const ItemPtr = *align(item_alignment) Item;
56+
57+
arena: std.heap.ArenaAllocator,
58+
free_list: ?NodePtr = null,
59+
60+
/// Creates a new memory pool.
61+
pub fn init(allocator: std.mem.Allocator) Pool {
62+
return .{ .arena = std.heap.ArenaAllocator.init(allocator) };
63+
}
64+
65+
/// Creates a new memory pool and pre-allocates `initial_size` items.
66+
/// This allows the up to `initial_size` active allocations before a
67+
/// `OutOfMemory` error happens when calling `create()`.
68+
pub fn initPreheated(allocator: std.mem.Allocator, initial_size: usize) MemoryPoolError!Pool {
69+
var pool = init(allocator);
70+
errdefer pool.deinit();
71+
72+
var i: usize = 0;
73+
while (i < initial_size) : (i += 1) {
74+
const raw_mem = try pool.allocNew();
75+
const free_node = @ptrCast(NodePtr, raw_mem);
76+
free_node.* = Node{
77+
.next = pool.free_list,
78+
};
79+
pool.free_list = free_node;
80+
}
81+
82+
return pool;
83+
}
84+
85+
/// Destroys the memory pool and frees all allocated memory.
86+
pub fn deinit(pool: *Pool) void {
87+
pool.arena.deinit();
88+
pool.* = undefined;
89+
}
90+
91+
/// Resets the memory pool and destroys all allocated items.
92+
/// This can be used to batch-destroy all objects without invalidating the memory pool.
93+
pub fn reset(pool: *Pool) void {
94+
// TODO: Potentially store all allocated objects in a list as well, allowing to
95+
// just move them into the free list instead of actually releasing the memory.
96+
const allocator = pool.arena.child_allocator;
97+
98+
// TODO: Replace with "pool.arena.reset()" when implemented.
99+
pool.arena.deinit();
100+
pool.arena = std.heap.ArenaAllocator.init(allocator);
101+
102+
pool.free_list = null;
103+
}
104+
105+
/// Creates a new item and adds it to the memory pool.
106+
pub fn create(pool: *Pool) !ItemPtr {
107+
const node = if (pool.free_list) |item| blk: {
108+
pool.free_list = item.next;
109+
break :blk item;
110+
} else if (pool_options.growable)
111+
@ptrCast(NodePtr, try pool.allocNew())
112+
else
113+
return error.OutOfMemory;
114+
115+
const ptr = @ptrCast(ItemPtr, node);
116+
ptr.* = undefined;
117+
return ptr;
118+
}
119+
120+
/// Destroys a previously created item.
121+
/// Only pass items to `ptr` that were previously created with `create()` of the same memory pool!
122+
pub fn destroy(pool: *Pool, ptr: ItemPtr) void {
123+
ptr.* = undefined;
124+
125+
const node = @ptrCast(NodePtr, ptr);
126+
node.* = Node{
127+
.next = pool.free_list,
128+
};
129+
pool.free_list = node;
130+
}
131+
132+
fn allocNew(pool: *Pool) MemoryPoolError!*align(item_alignment) [item_size]u8 {
133+
const mem = try pool.arena.allocator().alignedAlloc(u8, item_alignment, item_size);
134+
return mem[0..item_size]; // coerce slice to array pointer
135+
}
136+
};
137+
}
138+
139+
test "memory pool: basic" {
140+
var pool = MemoryPool(u32).init(std.testing.allocator);
141+
defer pool.deinit();
142+
143+
const p1 = try pool.create();
144+
const p2 = try pool.create();
145+
const p3 = try pool.create();
146+
147+
// Assert uniqueness
148+
try std.testing.expect(p1 != p2);
149+
try std.testing.expect(p1 != p3);
150+
try std.testing.expect(p2 != p3);
151+
152+
pool.destroy(p2);
153+
const p4 = try pool.create();
154+
155+
// Assert memory resuse
156+
try std.testing.expect(p2 == p4);
157+
}
158+
159+
test "memory pool: preheating (success)" {
160+
var pool = try MemoryPool(u32).initPreheated(std.testing.allocator, 4);
161+
defer pool.deinit();
162+
163+
_ = try pool.create();
164+
_ = try pool.create();
165+
_ = try pool.create();
166+
}
167+
168+
test "memory pool: preheating (failure)" {
169+
var failer = std.testing.FailingAllocator.init(std.testing.allocator, 0);
170+
try std.testing.expectError(error.OutOfMemory, MemoryPool(u32).initPreheated(failer.allocator(), 5));
171+
}
172+
173+
test "memory pool: growable" {
174+
var pool = try MemoryPoolExtra(u32, .{ .growable = false }).initPreheated(std.testing.allocator, 4);
175+
defer pool.deinit();
176+
177+
_ = try pool.create();
178+
_ = try pool.create();
179+
_ = try pool.create();
180+
_ = try pool.create();
181+
182+
try std.testing.expectError(error.OutOfMemory, pool.create());
183+
}

0 commit comments

Comments
 (0)