| ... | ... | @@ -1,26 +1,26 @@ |
| 1 | 1 | const std = @import("../std.zig"); |
| 2 | const Allocator = std.mem.Allocator; |
| 2 | 3 | const Alignment = std.mem.Alignment; |
| 4 | const MemoryPool = std.heap.MemoryPool; |
| 3 | 5 | |
| 4 | | const debug_mode = @import("builtin").mode == .Debug; |
| 5 | | |
| 6 | | pub const MemoryPoolError = error{OutOfMemory}; |
| 6 | /// Deprecated. |
| 7 | pub fn Managed(comptime Item: type) type { |
| 8 | return ExtraManaged(Item, .{ .alignment = null }); |
| 9 | } |
| 7 | 10 | |
| 8 | 11 | /// A memory pool that can allocate objects of a single type very quickly. |
| 9 | 12 | /// Use this when you need to allocate a lot of objects of the same type, |
| 10 | | /// because It outperforms general purpose allocators. |
| 11 | | pub fn MemoryPool(comptime Item: type) type { |
| 12 | | return MemoryPoolAligned(Item, .of(Item)); |
| 13 | /// because it outperforms general purpose allocators. |
| 14 | /// Allocated items are aligned to `alignment`-byte addresses or `@alignOf(Item)` |
| 15 | /// if `alignment` is `null`. |
| 16 | /// Functions that potentially allocate memory accept an `Allocator` parameter. |
| 17 | pub fn Aligned(comptime Item: type, comptime alignment: Alignment) type { |
| 18 | return Extra(Item, .{ .alignment = alignment }); |
| 13 | 19 | } |
| 14 | 20 | |
| 15 | | /// A memory pool that can allocate objects of a single type very quickly. |
| 16 | | /// Use this when you need to allocate a lot of objects of the same type, |
| 17 | | /// because It outperforms general purpose allocators. |
| 18 | | pub fn MemoryPoolAligned(comptime Item: type, comptime alignment: Alignment) type { |
| 19 | | if (@alignOf(Item) == comptime alignment.toByteUnits()) { |
| 20 | | return MemoryPoolExtra(Item, .{}); |
| 21 | | } else { |
| 22 | | return MemoryPoolExtra(Item, .{ .alignment = alignment }); |
| 23 | | } |
| 21 | /// Deprecated. |
| 22 | pub fn AlignedManaged(comptime Item: type, comptime alignment: Alignment) type { |
| 23 | return ExtraManaged(Item, .{ .alignment = alignment }); |
| 24 | 24 | } |
| 25 | 25 | |
| 26 | 26 | pub const Options = struct { |
| ... | ... | @@ -34,64 +34,70 @@ pub const Options = struct { |
| 34 | 34 | |
| 35 | 35 | /// A memory pool that can allocate objects of a single type very quickly. |
| 36 | 36 | /// Use this when you need to allocate a lot of objects of the same type, |
| 37 | | /// because It outperforms general purpose allocators. |
| 38 | | pub fn MemoryPoolExtra(comptime Item: type, comptime pool_options: Options) type { |
| 37 | /// because it outperforms general purpose allocators. |
| 38 | /// Functions that potentially allocate memory accept an `Allocator` parameter. |
| 39 | pub fn Extra(comptime Item: type, comptime pool_options: Options) type { |
| 40 | if (pool_options.alignment) |a| { |
| 41 | if (a.compare(.eq, .of(Item))) { |
| 42 | var new_options = pool_options; |
| 43 | new_options.alignment = null; |
| 44 | return Extra(Item, new_options); |
| 45 | } |
| 46 | } |
| 39 | 47 | return struct { |
| 40 | 48 | const Pool = @This(); |
| 41 | 49 | |
| 50 | arena_state: std.heap.ArenaAllocator.State, |
| 51 | free_list: std.SinglyLinkedList, |
| 52 | |
| 42 | 53 | /// Size of the memory pool items. This is not necessarily the same |
| 43 | 54 | /// as `@sizeOf(Item)` as the pool also uses the items for internal means. |
| 44 | 55 | pub const item_size = @max(@sizeOf(Node), @sizeOf(Item)); |
| 45 | 56 | |
| 46 | | // This needs to be kept in sync with Node. |
| 47 | | const node_alignment: Alignment = .of(*anyopaque); |
| 48 | | |
| 49 | 57 | /// Alignment of the memory pool items. This is not necessarily the same |
| 50 | 58 | /// as `@alignOf(Item)` as the pool also uses the items for internal means. |
| 51 | | pub const item_alignment: Alignment = node_alignment.max(pool_options.alignment orelse .of(Item)); |
| 59 | pub const item_alignment: Alignment = .max(pool_options.alignment orelse .of(Item), .of(Node)); |
| 52 | 60 | |
| 53 | | const Node = struct { |
| 54 | | next: ?*align(item_alignment.toByteUnits()) @This(), |
| 55 | | }; |
| 56 | | const NodePtr = *align(item_alignment.toByteUnits()) Node; |
| 61 | const Node = std.SinglyLinkedList.Node; |
| 57 | 62 | const ItemPtr = *align(item_alignment.toByteUnits()) Item; |
| 58 | 63 | |
| 59 | | arena: std.heap.ArenaAllocator, |
| 60 | | free_list: ?NodePtr = null, |
| 61 | | |
| 62 | | /// Creates a new memory pool. |
| 63 | | pub fn init(allocator: std.mem.Allocator) Pool { |
| 64 | | return .{ .arena = std.heap.ArenaAllocator.init(allocator) }; |
| 65 | | } |
| 64 | /// A MemoryPool containing no elements. |
| 65 | pub const empty: Pool = .{ |
| 66 | .arena_state = .{}, |
| 67 | .free_list = .{}, |
| 68 | }; |
| 66 | 69 | |
| 67 | | /// Creates a new memory pool and pre-allocates `initial_size` items. |
| 68 | | /// This allows the up to `initial_size` active allocations before a |
| 69 | | /// `OutOfMemory` error happens when calling `create()`. |
| 70 | | pub fn initPreheated(allocator: std.mem.Allocator, initial_size: usize) MemoryPoolError!Pool { |
| 71 | | var pool = init(allocator); |
| 72 | | errdefer pool.deinit(); |
| 73 | | try pool.preheat(initial_size); |
| 70 | /// Creates a new memory pool and pre-allocates `num` items. |
| 71 | /// This allows up to `num` active allocations before an |
| 72 | /// `OutOfMemory` error might happen when calling `create()`. |
| 73 | pub fn initCapacity(allocator: Allocator, num: usize) Allocator.Error!Pool { |
| 74 | var pool: Pool = .empty; |
| 75 | errdefer pool.deinit(allocator); |
| 76 | try pool.addCapacity(allocator, num); |
| 74 | 77 | return pool; |
| 75 | 78 | } |
| 76 | 79 | |
| 77 | 80 | /// Destroys the memory pool and frees all allocated memory. |
| 78 | | pub fn deinit(pool: *Pool) void { |
| 79 | | pool.arena.deinit(); |
| 81 | pub fn deinit(pool: *Pool, allocator: Allocator) void { |
| 82 | pool.arena_state.promote(allocator).deinit(); |
| 80 | 83 | pool.* = undefined; |
| 81 | 84 | } |
| 82 | 85 | |
| 83 | | /// Preheats the memory pool by pre-allocating `size` items. |
| 84 | | /// This allows up to `size` active allocations before an |
| 86 | pub fn toManaged(pool: Pool, allocator: Allocator) ExtraManaged(Item, pool_options) { |
| 87 | return .{ |
| 88 | .allocator = allocator, |
| 89 | .unmanaged = pool, |
| 90 | }; |
| 91 | } |
| 92 | |
| 93 | /// Pre-allocates `num` items and adds them to the memory pool. |
| 94 | /// This allows at least `num` active allocations before an |
| 85 | 95 | /// `OutOfMemory` error might happen when calling `create()`. |
| 86 | | pub fn preheat(pool: *Pool, size: usize) MemoryPoolError!void { |
| 96 | pub fn addCapacity(pool: *Pool, allocator: Allocator, num: usize) Allocator.Error!void { |
| 87 | 97 | var i: usize = 0; |
| 88 | | while (i < size) : (i += 1) { |
| 89 | | const raw_mem = try pool.allocNew(); |
| 90 | | const free_node = @as(NodePtr, @ptrCast(raw_mem)); |
| 91 | | free_node.* = Node{ |
| 92 | | .next = pool.free_list, |
| 93 | | }; |
| 94 | | pool.free_list = free_node; |
| 98 | while (i < num) : (i += 1) { |
| 99 | const memory = try pool.allocNew(allocator); |
| 100 | pool.free_list.prepend(@ptrCast(memory)); |
| 95 | 101 | } |
| 96 | 102 | } |
| 97 | 103 | |
| ... | ... | @@ -106,28 +112,29 @@ pub fn MemoryPoolExtra(comptime Item: type, comptime pool_options: Options) type |
| 106 | 112 | /// be slower. |
| 107 | 113 | /// |
| 108 | 114 | /// NOTE: If `mode` is `free_all`, the function will always return `true`. |
| 109 | | pub fn reset(pool: *Pool, mode: ResetMode) bool { |
| 115 | pub fn reset(pool: *Pool, allocator: Allocator, mode: ResetMode) bool { |
| 110 | 116 | // TODO: Potentially store all allocated objects in a list as well, allowing to |
| 111 | 117 | // just move them into the free list instead of actually releasing the memory. |
| 112 | 118 | |
| 113 | | const reset_successful = pool.arena.reset(mode); |
| 119 | var arena = pool.arena_state.promote(allocator); |
| 120 | defer pool.arena_state = arena.state; |
| 114 | 121 | |
| 115 | | pool.free_list = null; |
| 122 | const reset_successful = arena.reset(mode); |
| 123 | pool.free_list = .{}; |
| 116 | 124 | |
| 117 | 125 | return reset_successful; |
| 118 | 126 | } |
| 119 | 127 | |
| 120 | 128 | /// Creates a new item and adds it to the memory pool. |
| 121 | | pub fn create(pool: *Pool) !ItemPtr { |
| 122 | | const node = if (pool.free_list) |item| blk: { |
| 123 | | pool.free_list = item.next; |
| 124 | | break :blk item; |
| 125 | | } else if (pool_options.growable) |
| 126 | | @as(NodePtr, @ptrCast(try pool.allocNew())) |
| 129 | /// `allocator` may be `undefined` if pool is not `growable`. |
| 130 | pub fn create(pool: *Pool, allocator: Allocator) Allocator.Error!ItemPtr { |
| 131 | const ptr: ItemPtr = if (pool.free_list.popFirst()) |node| |
| 132 | @ptrCast(@alignCast(node)) |
| 133 | else if (pool_options.growable) |
| 134 | @ptrCast(try pool.allocNew(allocator)) |
| 127 | 135 | else |
| 128 | 136 | return error.OutOfMemory; |
| 129 | 137 | |
| 130 | | const ptr = @as(ItemPtr, @ptrCast(node)); |
| 131 | 138 | ptr.* = undefined; |
| 132 | 139 | return ptr; |
| 133 | 140 | } |
| ... | ... | @@ -136,87 +143,238 @@ pub fn MemoryPoolExtra(comptime Item: type, comptime pool_options: Options) type |
| 136 | 143 | /// Only pass items to `ptr` that were previously created with `create()` of the same memory pool! |
| 137 | 144 | pub fn destroy(pool: *Pool, ptr: ItemPtr) void { |
| 138 | 145 | ptr.* = undefined; |
| 146 | pool.free_list.prepend(@ptrCast(ptr)); |
| 147 | } |
| 139 | 148 | |
| 140 | | const node = @as(NodePtr, @ptrCast(ptr)); |
| 141 | | node.* = Node{ |
| 142 | | .next = pool.free_list, |
| 143 | | }; |
| 144 | | pool.free_list = node; |
| 149 | fn allocNew(pool: *Pool, allocator: Allocator) Allocator.Error!*align(item_alignment.toByteUnits()) [item_size]u8 { |
| 150 | var arena = pool.arena_state.promote(allocator); |
| 151 | defer pool.arena_state = arena.state; |
| 152 | const memory = try arena.allocator().alignedAlloc(u8, item_alignment, item_size); |
| 153 | return memory[0..item_size]; |
| 154 | } |
| 155 | }; |
| 156 | } |
| 157 | |
| 158 | /// Deprecated. |
| 159 | pub fn ExtraManaged(comptime Item: type, comptime pool_options: Options) type { |
| 160 | if (pool_options.alignment) |a| { |
| 161 | if (a.compare(.eq, .of(Item))) { |
| 162 | var new_options = pool_options; |
| 163 | new_options.alignment = null; |
| 164 | return ExtraManaged(Item, new_options); |
| 165 | } |
| 166 | } |
| 167 | return struct { |
| 168 | const Pool = @This(); |
| 169 | |
| 170 | allocator: Allocator, |
| 171 | unmanaged: Unmanaged, |
| 172 | |
| 173 | pub const Unmanaged = Extra(Item, pool_options); |
| 174 | pub const item_size = Unmanaged.item_size; |
| 175 | pub const item_alignment = Unmanaged.item_alignment; |
| 176 | |
| 177 | const ItemPtr = Unmanaged.ItemPtr; |
| 178 | |
| 179 | /// Creates a new memory pool. |
| 180 | pub fn init(allocator: Allocator) Pool { |
| 181 | return Unmanaged.empty.toManaged(allocator); |
| 182 | } |
| 183 | |
| 184 | /// Creates a new memory pool and pre-allocates `num` items. |
| 185 | /// This allows up to `num` active allocations before an |
| 186 | /// `OutOfMemory` error might happen when calling `create()`. |
| 187 | pub fn initCapacity(allocator: Allocator, num: usize) Allocator.Error!Pool { |
| 188 | return (try Unmanaged.initCapacity(allocator, num)).toManaged(allocator); |
| 189 | } |
| 190 | |
| 191 | /// Destroys the memory pool and frees all allocated memory. |
| 192 | pub fn deinit(pool: *Pool) void { |
| 193 | pool.unmanaged.deinit(pool.allocator); |
| 194 | pool.* = undefined; |
| 195 | } |
| 196 | |
| 197 | /// Pre-allocates `num` items and adds them to the memory pool. |
| 198 | /// This allows at least `num` active allocations before an |
| 199 | /// `OutOfMemory` error might happen when calling `create()`. |
| 200 | pub fn addCapacity(pool: *Pool, num: usize) Allocator.Error!void { |
| 201 | return pool.unmanaged.addCapacity(pool.allocator, num); |
| 202 | } |
| 203 | |
| 204 | pub const ResetMode = Unmanaged.ResetMode; |
| 205 | |
| 206 | /// Resets the memory pool and destroys all allocated items. |
| 207 | /// This can be used to batch-destroy all objects without invalidating the memory pool. |
| 208 | /// |
| 209 | /// The function will return whether the reset operation was successful or not. |
| 210 | /// If the reallocation failed `false` is returned. The pool will still be fully |
| 211 | /// functional in that case, all memory is released. Future allocations just might |
| 212 | /// be slower. |
| 213 | /// |
| 214 | /// NOTE: If `mode` is `free_all`, the function will always return `true`. |
| 215 | pub fn reset(pool: *Pool, mode: ResetMode) bool { |
| 216 | return pool.unmanaged.reset(pool.allocator, mode); |
| 145 | 217 | } |
| 146 | 218 | |
| 147 | | fn allocNew(pool: *Pool) MemoryPoolError!*align(item_alignment.toByteUnits()) [item_size]u8 { |
| 148 | | const mem = try pool.arena.allocator().alignedAlloc(u8, item_alignment, item_size); |
| 149 | | return mem[0..item_size]; // coerce slice to array pointer |
| 219 | /// Creates a new item and adds it to the memory pool. |
| 220 | pub fn create(pool: *Pool) Allocator.Error!ItemPtr { |
| 221 | return pool.unmanaged.create(pool.allocator); |
| 222 | } |
| 223 | |
| 224 | /// Destroys a previously created item. |
| 225 | /// Only pass items to `ptr` that were previously created with `create()` of the same memory pool! |
| 226 | pub fn destroy(pool: *Pool, ptr: ItemPtr) void { |
| 227 | return pool.unmanaged.destroy(ptr); |
| 228 | } |
| 229 | |
| 230 | fn allocNew(pool: *Pool) Allocator.Error!*align(item_alignment) [item_size]u8 { |
| 231 | return pool.unmanaged.allocNew(pool.allocator); |
| 150 | 232 | } |
| 151 | 233 | }; |
| 152 | 234 | } |
| 153 | 235 | |
| 154 | 236 | test "basic" { |
| 155 | | var pool = MemoryPool(u32).init(std.testing.allocator); |
| 156 | | defer pool.deinit(); |
| 237 | const a = std.testing.allocator; |
| 238 | |
| 239 | { |
| 240 | var pool: MemoryPool(u32) = .empty; |
| 241 | defer pool.deinit(a); |
| 157 | 242 | |
| 158 | | const p1 = try pool.create(); |
| 159 | | const p2 = try pool.create(); |
| 160 | | const p3 = try pool.create(); |
| 243 | const p1 = try pool.create(a); |
| 244 | const p2 = try pool.create(a); |
| 245 | const p3 = try pool.create(a); |
| 161 | 246 | |
| 162 | | // Assert uniqueness |
| 163 | | try std.testing.expect(p1 != p2); |
| 164 | | try std.testing.expect(p1 != p3); |
| 165 | | try std.testing.expect(p2 != p3); |
| 247 | // Assert uniqueness |
| 248 | try std.testing.expect(p1 != p2); |
| 249 | try std.testing.expect(p1 != p3); |
| 250 | try std.testing.expect(p2 != p3); |
| 251 | |
| 252 | pool.destroy(p2); |
| 253 | const p4 = try pool.create(a); |
| 254 | |
| 255 | // Assert memory reuse |
| 256 | try std.testing.expect(p2 == p4); |
| 257 | } |
| 166 | 258 | |
| 167 | | pool.destroy(p2); |
| 168 | | const p4 = try pool.create(); |
| 259 | { |
| 260 | var pool: Managed(u32) = .init(std.testing.allocator); |
| 261 | defer pool.deinit(); |
| 169 | 262 | |
| 170 | | // Assert memory reuse |
| 171 | | try std.testing.expect(p2 == p4); |
| 263 | const p1 = try pool.create(); |
| 264 | const p2 = try pool.create(); |
| 265 | const p3 = try pool.create(); |
| 266 | |
| 267 | // Assert uniqueness |
| 268 | try std.testing.expect(p1 != p2); |
| 269 | try std.testing.expect(p1 != p3); |
| 270 | try std.testing.expect(p2 != p3); |
| 271 | |
| 272 | pool.destroy(p2); |
| 273 | const p4 = try pool.create(); |
| 274 | |
| 275 | // Assert memory reuse |
| 276 | try std.testing.expect(p2 == p4); |
| 277 | } |
| 172 | 278 | } |
| 173 | 279 | |
| 174 | | test "preheating (success)" { |
| 175 | | var pool = try MemoryPool(u32).initPreheated(std.testing.allocator, 4); |
| 176 | | defer pool.deinit(); |
| 280 | test "initCapacity (success)" { |
| 281 | const a = std.testing.allocator; |
| 282 | |
| 283 | { |
| 284 | var pool: MemoryPool(u32) = try .initCapacity(a, 4); |
| 285 | defer pool.deinit(a); |
| 286 | |
| 287 | _ = try pool.create(a); |
| 288 | _ = try pool.create(a); |
| 289 | _ = try pool.create(a); |
| 290 | } |
| 291 | |
| 292 | { |
| 293 | var pool: Managed(u32) = try .initCapacity(a, 4); |
| 294 | defer pool.deinit(); |
| 177 | 295 | |
| 178 | | _ = try pool.create(); |
| 179 | | _ = try pool.create(); |
| 180 | | _ = try pool.create(); |
| 296 | _ = try pool.create(); |
| 297 | _ = try pool.create(); |
| 298 | _ = try pool.create(); |
| 299 | } |
| 181 | 300 | } |
| 182 | 301 | |
| 183 | | test "preheating (failure)" { |
| 302 | test "initCapacity (failure)" { |
| 184 | 303 | const failer = std.testing.failing_allocator; |
| 185 | | try std.testing.expectError(error.OutOfMemory, MemoryPool(u32).initPreheated(failer, 5)); |
| 304 | try std.testing.expectError(error.OutOfMemory, MemoryPool(u32).initCapacity(failer, 5)); |
| 305 | try std.testing.expectError(error.OutOfMemory, Managed(u32).initCapacity(failer, 5)); |
| 186 | 306 | } |
| 187 | 307 | |
| 188 | 308 | test "growable" { |
| 189 | | var pool = try MemoryPoolExtra(u32, .{ .growable = false }).initPreheated(std.testing.allocator, 4); |
| 190 | | defer pool.deinit(); |
| 309 | const a = std.testing.allocator; |
| 191 | 310 | |
| 192 | | _ = try pool.create(); |
| 193 | | _ = try pool.create(); |
| 194 | | _ = try pool.create(); |
| 195 | | _ = try pool.create(); |
| 311 | { |
| 312 | var pool: Extra(u32, .{ .growable = false }) = try .initCapacity(a, 4); |
| 313 | defer pool.deinit(a); |
| 314 | |
| 315 | _ = try pool.create(a); |
| 316 | _ = try pool.create(a); |
| 317 | _ = try pool.create(a); |
| 318 | _ = try pool.create(a); |
| 319 | |
| 320 | try std.testing.expectError(error.OutOfMemory, pool.create(a)); |
| 321 | } |
| 196 | 322 | |
| 197 | | try std.testing.expectError(error.OutOfMemory, pool.create()); |
| 323 | { |
| 324 | var pool: ExtraManaged(u32, .{ .growable = false }) = try .initCapacity(a, 4); |
| 325 | defer pool.deinit(); |
| 326 | |
| 327 | _ = try pool.create(); |
| 328 | _ = try pool.create(); |
| 329 | _ = try pool.create(); |
| 330 | _ = try pool.create(); |
| 331 | |
| 332 | try std.testing.expectError(error.OutOfMemory, pool.create()); |
| 333 | } |
| 198 | 334 | } |
| 199 | 335 | |
| 200 | 336 | test "greater than pointer default alignment" { |
| 201 | 337 | const Foo = struct { |
| 202 | 338 | data: u64 align(16), |
| 203 | 339 | }; |
| 340 | const a = std.testing.allocator; |
| 341 | |
| 342 | { |
| 343 | var pool: MemoryPool(Foo) = .empty; |
| 344 | defer pool.deinit(a); |
| 345 | |
| 346 | const foo: *Foo = try pool.create(a); |
| 347 | pool.destroy(foo); |
| 348 | } |
| 204 | 349 | |
| 205 | | var pool = MemoryPool(Foo).init(std.testing.allocator); |
| 206 | | defer pool.deinit(); |
| 350 | { |
| 351 | var pool: Managed(Foo) = .init(a); |
| 352 | defer pool.deinit(); |
| 207 | 353 | |
| 208 | | const foo: *Foo = try pool.create(); |
| 209 | | _ = foo; |
| 354 | const foo: *Foo = try pool.create(); |
| 355 | pool.destroy(foo); |
| 356 | } |
| 210 | 357 | } |
| 211 | 358 | |
| 212 | 359 | test "greater than pointer manual alignment" { |
| 213 | 360 | const Foo = struct { |
| 214 | 361 | data: u64, |
| 215 | 362 | }; |
| 363 | const a = std.testing.allocator; |
| 364 | |
| 365 | { |
| 366 | var pool: Aligned(Foo, .@"16") = .empty; |
| 367 | defer pool.deinit(a); |
| 216 | 368 | |
| 217 | | var pool = MemoryPoolAligned(Foo, .@"16").init(std.testing.allocator); |
| 218 | | defer pool.deinit(); |
| 369 | const foo: *align(16) Foo = try pool.create(a); |
| 370 | pool.destroy(foo); |
| 371 | } |
| 219 | 372 | |
| 220 | | const foo: *align(16) Foo = try pool.create(); |
| 221 | | _ = foo; |
| 373 | { |
| 374 | var pool: AlignedManaged(Foo, .@"16") = .init(a); |
| 375 | defer pool.deinit(); |
| 376 | |
| 377 | const foo: *align(16) Foo = try pool.create(); |
| 378 | pool.destroy(foo); |
| 379 | } |
| 222 | 380 | } |