authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-02-04 23:05:06-08:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-02-06 14:23:23-08:00
log8ff7481e8245f916966973e2c830bc3fb5e4c748
tree4b74d2dcaf1899d1395023ddc3b91c46f3937200
parent1a6d87d699a9e0867f3a50566dc72b3e2347d8d1

std.heap.GeneralPurposeAllocator: inline small allocation metadata

Put the small allocation metadata directly into the (large) pages allocated.

1 files changed, 261 insertions(+), 531 deletions(-)

lib/std/heap/general_purpose_allocator.zig+261-531
...@@ -99,12 +99,14 @@ const math = std.math;...@@ -99,12 +99,14 @@ const math = std.math;
99const assert = std.debug.assert;99const assert = std.debug.assert;
100const mem = std.mem;100const mem = std.mem;
101const Allocator = std.mem.Allocator;101const Allocator = std.mem.Allocator;
102const StackTrace = std.builtin.StackTrace;
103
102const page_size: usize = @max(std.heap.page_size_max, switch (builtin.os.tag) {104const page_size: usize = @max(std.heap.page_size_max, switch (builtin.os.tag) {
103 .windows => 64 * 1024, // Makes `std.heap.PageAllocator` take the happy path.105 .windows => 64 * 1024, // Makes `std.heap.PageAllocator` take the happy path.
104 .wasi => 64 * 1024, // Max alignment supported by `std.heap.WasmAllocator`.106 .wasi => 64 * 1024, // Max alignment supported by `std.heap.WasmAllocator`.
105 else => 2 * 1024 * 1024, // Avoids too many active mappings when `page_size_max` is low.107 else => 2 * 1024 * 1024, // Avoids too many active mappings when `page_size_max` is low.
106});108});
107const StackTrace = std.builtin.StackTrace;109const page_align: mem.Alignment = .fromByteUnits(page_size);
108110
109/// Integer type for pointing to slots in a small allocation111/// Integer type for pointing to slots in a small allocation
110const SlotIndex = std.meta.Int(.unsigned, math.log2(page_size) + 1);112const SlotIndex = std.meta.Int(.unsigned, math.log2(page_size) + 1);
...@@ -168,29 +170,23 @@ pub const Check = enum { ok, leak };...@@ -168,29 +170,23 @@ pub const Check = enum { ok, leak };
168pub fn GeneralPurposeAllocator(comptime config: Config) type {170pub fn GeneralPurposeAllocator(comptime config: Config) type {
169 return struct {171 return struct {
170 backing_allocator: Allocator = std.heap.page_allocator,172 backing_allocator: Allocator = std.heap.page_allocator,
171 buckets: [small_bucket_count]Buckets = [1]Buckets{Buckets{}} ** small_bucket_count,173 /// Tracks the active bucket, which is the one that has free slots in it.
172 cur_buckets: [small_bucket_count]?*BucketHeader = [1]?*BucketHeader{null} ** small_bucket_count,174 buckets: [small_bucket_count]?*BucketHeader = [1]?*BucketHeader{null} ** small_bucket_count,
173 large_allocations: LargeAllocTable = .{},175 large_allocations: LargeAllocTable = .empty,
174 empty_buckets: if (config.retain_metadata) Buckets else void =
175 if (config.retain_metadata) Buckets{} else {},
176 bucket_node_pool: std.heap.MemoryPool(Buckets.Node) = std.heap.MemoryPool(Buckets.Node).init(std.heap.page_allocator),
177
178 total_requested_bytes: @TypeOf(total_requested_bytes_init) = total_requested_bytes_init,176 total_requested_bytes: @TypeOf(total_requested_bytes_init) = total_requested_bytes_init,
179 requested_memory_limit: @TypeOf(requested_memory_limit_init) = requested_memory_limit_init,177 requested_memory_limit: @TypeOf(requested_memory_limit_init) = requested_memory_limit_init,
180
181 mutex: @TypeOf(mutex_init) = mutex_init,178 mutex: @TypeOf(mutex_init) = mutex_init,
182179
183 const Self = @This();180 const Self = @This();
184181
185 /// The initial state of a `GeneralPurposeAllocator`, containing no182 pub const init: Self = .{};
186 /// allocations and backed by the system page allocator.183
187 pub const init: Self = .{184 /// These can be derived from size_class_index but the calculation is nontrivial.
188 .backing_allocator = std.heap.page_allocator,185 const slot_counts: [small_bucket_count]SlotIndex = init: {
189 .buckets = [1]Buckets{.{}} ** small_bucket_count,186 @setEvalBranchQuota(10000);
190 .cur_buckets = [1]?*BucketHeader{null} ** small_bucket_count,187 var result: [small_bucket_count]SlotIndex = undefined;
191 .large_allocations = .{},188 for (&result, 0..) |*elem, i| elem.* = calculateSlotCount(i);
192 .empty_buckets = if (config.retain_metadata) .{} else {},189 break :init result;
193 .bucket_node_pool = .init(std.heap.page_allocator),
194 };190 };
195191
196 const total_requested_bytes_init = if (config.enable_memory_limit) @as(usize, 0) else {};192 const total_requested_bytes_init = if (config.enable_memory_limit) @as(usize, 0) else {};
...@@ -204,8 +200,8 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {...@@ -204,8 +200,8 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {
204 DummyMutex{};200 DummyMutex{};
205201
206 const DummyMutex = struct {202 const DummyMutex = struct {
207 fn lock(_: *DummyMutex) void {}203 inline fn lock(_: *DummyMutex) void {}
208 fn unlock(_: *DummyMutex) void {}204 inline fn unlock(_: *DummyMutex) void {}
209 };205 };
210206
211 const stack_n = config.stack_trace_frames;207 const stack_n = config.stack_trace_frames;
...@@ -223,7 +219,6 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {...@@ -223,7 +219,6 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {
223 return std.math.order(@intFromPtr(a.page), @intFromPtr(b.page));219 return std.math.order(@intFromPtr(a.page), @intFromPtr(b.page));
224 }220 }
225 }.compare;221 }.compare;
226 const Buckets = std.Treap(*BucketHeader, bucketCompare);
227222
228 const LargeAlloc = struct {223 const LargeAlloc = struct {
229 bytes: []u8,224 bytes: []u8,
...@@ -259,46 +254,50 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {...@@ -259,46 +254,50 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {
259 };254 };
260 const LargeAllocTable = std.AutoHashMapUnmanaged(usize, LargeAlloc);255 const LargeAllocTable = std.AutoHashMapUnmanaged(usize, LargeAlloc);
261256
262 // Bucket: In memory, in order:257 /// Bucket: In memory, in order:
263 // * BucketHeader258 /// * BucketHeader
264 // * bucket_used_bits: [N]u8, // 1 bit for every slot; 1 byte for every 8 slots259 /// * bucket_used_bits: [N]u8, // 1 bit for every slot; 1 byte for every 8 slots
265 // -- below only exists when config.safety is true --260 /// -- below only exists when config.safety is true --
266 // * requested_sizes: [N]LargestSizeClassInt // 1 int for every slot261 /// * requested_sizes: [N]LargestSizeClassInt // 1 int for every slot
267 // * log2_ptr_aligns: [N]u8 // 1 byte for every slot262 /// * log2_ptr_aligns: [N]u8 // 1 byte for every slot
268 // -- above only exists when config.safety is true --263 /// -- above only exists when config.safety is true --
269 // * stack_trace_addresses: [N]usize, // traces_per_slot for every allocation264 /// * stack_trace_addresses: [N]usize, // traces_per_slot for every allocation
270
271 const BucketHeader = struct {265 const BucketHeader = struct {
272 page: [*]align(page_size) u8,266 allocated_count: SlotIndex,
273 alloc_cursor: SlotIndex,267 freed_count: SlotIndex,
274 used_count: SlotIndex,268 prev: ?*BucketHeader,
269
270 fn fromPage(page_addr: usize, slot_count: usize) *BucketHeader {
271 const unaligned = page_addr + page_size - bucketSize(slot_count);
272 return @ptrFromInt(unaligned & ~(@as(usize, @alignOf(BucketHeader)) - 1));
273 }
275274
275 // TODO use usize instead of u8
276 fn usedBits(bucket: *BucketHeader, index: usize) *u8 {276 fn usedBits(bucket: *BucketHeader, index: usize) *u8 {
277 return @as(*u8, @ptrFromInt(@intFromPtr(bucket) + @sizeOf(BucketHeader) + index));277 // TODO avoid ptr to int
278 return @ptrFromInt(@intFromPtr(bucket) + @sizeOf(BucketHeader) + index);
278 }279 }
279280
280 fn requestedSizes(bucket: *BucketHeader, size_class: usize) []LargestSizeClassInt {281 fn requestedSizes(bucket: *BucketHeader, slot_count: usize) []LargestSizeClassInt {
281 if (!config.safety) @compileError("requested size is only stored when safety is enabled");282 if (!config.safety) @compileError("requested size is only stored when safety is enabled");
282 const start_ptr = @as([*]u8, @ptrCast(bucket)) + bucketRequestedSizesStart(size_class);283 const start_ptr = @as([*]u8, @ptrCast(bucket)) + bucketRequestedSizesStart(slot_count);
283 const sizes = @as([*]LargestSizeClassInt, @ptrCast(@alignCast(start_ptr)));284 const sizes = @as([*]LargestSizeClassInt, @ptrCast(@alignCast(start_ptr)));
284 const slot_count = @divExact(page_size, size_class);
285 return sizes[0..slot_count];285 return sizes[0..slot_count];
286 }286 }
287287
288 fn log2PtrAligns(bucket: *BucketHeader, size_class: usize) []mem.Alignment {288 fn log2PtrAligns(bucket: *BucketHeader, slot_count: usize) []mem.Alignment {
289 if (!config.safety) @compileError("requested size is only stored when safety is enabled");289 if (!config.safety) @compileError("requested size is only stored when safety is enabled");
290 const aligns_ptr = @as([*]u8, @ptrCast(bucket)) + bucketAlignsStart(size_class);290 const aligns_ptr = @as([*]u8, @ptrCast(bucket)) + bucketAlignsStart(slot_count);
291 const slot_count = @divExact(page_size, size_class);
292 return @ptrCast(aligns_ptr[0..slot_count]);291 return @ptrCast(aligns_ptr[0..slot_count]);
293 }292 }
294293
295 fn stackTracePtr(294 fn stackTracePtr(
296 bucket: *BucketHeader,295 bucket: *BucketHeader,
297 size_class: usize,296 slot_count: usize,
298 slot_index: SlotIndex,297 slot_index: SlotIndex,
299 trace_kind: TraceKind,298 trace_kind: TraceKind,
300 ) *[stack_n]usize {299 ) *[stack_n]usize {
301 const start_ptr = @as([*]u8, @ptrCast(bucket)) + bucketStackFramesStart(size_class);300 const start_ptr = @as([*]u8, @ptrCast(bucket)) + bucketStackFramesStart(slot_count);
302 const addr = start_ptr + one_trace_size * traces_per_slot * slot_index +301 const addr = start_ptr + one_trace_size * traces_per_slot * slot_index +
303 @intFromEnum(trace_kind) * @as(usize, one_trace_size);302 @intFromEnum(trace_kind) * @as(usize, one_trace_size);
304 return @ptrCast(@alignCast(addr));303 return @ptrCast(@alignCast(addr));
...@@ -307,21 +306,15 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {...@@ -307,21 +306,15 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {
307 fn captureStackTrace(306 fn captureStackTrace(
308 bucket: *BucketHeader,307 bucket: *BucketHeader,
309 ret_addr: usize,308 ret_addr: usize,
310 size_class: usize,309 slot_count: usize,
311 slot_index: SlotIndex,310 slot_index: SlotIndex,
312 trace_kind: TraceKind,311 trace_kind: TraceKind,
313 ) void {312 ) void {
314 // Initialize them to 0. When determining the count we must look313 // Initialize them to 0. When determining the count we must look
315 // for non zero addresses.314 // for non zero addresses.
316 const stack_addresses = bucket.stackTracePtr(size_class, slot_index, trace_kind);315 const stack_addresses = bucket.stackTracePtr(slot_count, slot_index, trace_kind);
317 collectStackTrace(ret_addr, stack_addresses);316 collectStackTrace(ret_addr, stack_addresses);
318 }317 }
319
320 /// Only valid for buckets within `empty_buckets`, and relies on the `alloc_cursor`
321 /// of empty buckets being set to `slot_count` when they are added to `empty_buckets`
322 fn emptyBucketSizeClass(bucket: *BucketHeader) usize {
323 return @divExact(page_size, bucket.alloc_cursor);
324 }
325 };318 };
326319
327 pub fn allocator(self: *Self) Allocator {320 pub fn allocator(self: *Self) Allocator {
...@@ -338,64 +331,76 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {...@@ -338,64 +331,76 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {
338331
339 fn bucketStackTrace(332 fn bucketStackTrace(
340 bucket: *BucketHeader,333 bucket: *BucketHeader,
341 size_class: usize,334 slot_count: usize,
342 slot_index: SlotIndex,335 slot_index: SlotIndex,
343 trace_kind: TraceKind,336 trace_kind: TraceKind,
344 ) StackTrace {337 ) StackTrace {
345 const stack_addresses = bucket.stackTracePtr(size_class, slot_index, trace_kind);338 const stack_addresses = bucket.stackTracePtr(slot_count, slot_index, trace_kind);
346 var len: usize = 0;339 var len: usize = 0;
347 while (len < stack_n and stack_addresses[len] != 0) {340 while (len < stack_n and stack_addresses[len] != 0) {
348 len += 1;341 len += 1;
349 }342 }
350 return StackTrace{343 return .{
351 .instruction_addresses = stack_addresses,344 .instruction_addresses = stack_addresses,
352 .index = len,345 .index = len,
353 };346 };
354 }347 }
355348
356 fn bucketRequestedSizesStart(size_class: usize) usize {349 fn bucketRequestedSizesStart(slot_count: usize) usize {
357 if (!config.safety) @compileError("requested sizes are not stored unless safety is enabled");350 if (!config.safety) @compileError("requested sizes are not stored unless safety is enabled");
358 return mem.alignForward(351 return mem.alignForward(
359 usize,352 usize,
360 @sizeOf(BucketHeader) + usedBitsCount(size_class),353 @sizeOf(BucketHeader) + usedBitsCount(slot_count),
361 @alignOf(LargestSizeClassInt),354 @alignOf(LargestSizeClassInt),
362 );355 );
363 }356 }
364357
365 fn bucketAlignsStart(size_class: usize) usize {358 fn bucketAlignsStart(slot_count: usize) usize {
366 if (!config.safety) @compileError("requested sizes are not stored unless safety is enabled");359 if (!config.safety) @compileError("requested sizes are not stored unless safety is enabled");
367 const slot_count = @divExact(page_size, size_class);360 return bucketRequestedSizesStart(slot_count) + (@sizeOf(LargestSizeClassInt) * slot_count);
368 return bucketRequestedSizesStart(size_class) + (@sizeOf(LargestSizeClassInt) * slot_count);
369 }361 }
370362
371 fn bucketStackFramesStart(size_class: usize) usize {363 fn bucketStackFramesStart(slot_count: usize) usize {
372 const unaligned_start = if (config.safety) blk: {364 const unaligned_start = if (config.safety)
373 const slot_count = @divExact(page_size, size_class);365 bucketAlignsStart(slot_count) + slot_count
374 break :blk bucketAlignsStart(size_class) + slot_count;366 else
375 } else @sizeOf(BucketHeader) + usedBitsCount(size_class);367 @sizeOf(BucketHeader) + usedBitsCount(slot_count);
376 return mem.alignForward(368 return mem.alignForward(usize, unaligned_start, @alignOf(usize));
377 usize,
378 unaligned_start,
379 @alignOf(usize),
380 );
381 }369 }
382370
383 fn bucketSize(size_class: usize) usize {371 fn bucketSize(slot_count: usize) usize {
384 const slot_count = @divExact(page_size, size_class);372 return bucketStackFramesStart(slot_count) + one_trace_size * traces_per_slot * slot_count;
385 return bucketStackFramesStart(size_class) + one_trace_size * traces_per_slot * slot_count;
386 }373 }
387374
388 fn usedBitsCount(size_class: usize) usize {375 /// This is executed only at compile-time to prepopulate a lookup table.
389 const slot_count = @divExact(page_size, size_class);376 fn calculateSlotCount(size_class_index: usize) SlotIndex {
390 if (slot_count < 8) return 1;377 const size_class = @as(usize, 1) << @as(u6, @intCast(size_class_index));
391 return @divExact(slot_count, 8);378 var lower: usize = 8;
379 var upper: usize = (page_size - bucketSize(lower)) / size_class;
380 while (upper > lower) {
381 const proposed: usize = lower + (upper - lower) / 2;
382 if (proposed == lower) return lower;
383 const slots_end = proposed * size_class;
384 const header_begin = mem.alignForward(usize, slots_end, @alignOf(BucketHeader));
385 const bucket_size = bucketSize(proposed);
386 const end = header_begin + bucket_size;
387 if (end > page_size) {
388 upper = proposed - 1;
389 } else {
390 lower = proposed;
391 }
392 }
393 return lower;
392 }394 }
393395
394 fn detectLeaksInBucket(396 fn usedBitsCount(slot_count: usize) usize {
395 bucket: *BucketHeader,397 assert(slot_count >= 8);
396 size_class: usize,398 return (slot_count + 7) / 8;
397 used_bits_count: usize,399 }
398 ) bool {400
401 fn detectLeaksInBucket(bucket: *BucketHeader, size_class_index: usize, used_bits_count: usize) bool {
402 const size_class = @as(usize, 1) << @as(u6, @intCast(size_class_index));
403 const slot_count = slot_counts[size_class_index];
399 var leaks = false;404 var leaks = false;
400 var used_bits_byte: usize = 0;405 var used_bits_byte: usize = 0;
401 while (used_bits_byte < used_bits_count) : (used_bits_byte += 1) {406 while (used_bits_byte < used_bits_count) : (used_bits_byte += 1) {
...@@ -405,12 +410,11 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {...@@ -405,12 +410,11 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {
405 while (true) : (bit_index += 1) {410 while (true) : (bit_index += 1) {
406 const is_used = @as(u1, @truncate(used_byte >> bit_index)) != 0;411 const is_used = @as(u1, @truncate(used_byte >> bit_index)) != 0;
407 if (is_used) {412 if (is_used) {
408 const slot_index = @as(SlotIndex, @intCast(used_bits_byte * 8 + bit_index));413 const slot_index: SlotIndex = @intCast(used_bits_byte * 8 + bit_index);
409 const stack_trace = bucketStackTrace(bucket, size_class, slot_index, .alloc);414 const stack_trace = bucketStackTrace(bucket, slot_count, slot_index, .alloc);
410 const addr = bucket.page + slot_index * size_class;415 const page_addr = @intFromPtr(bucket) & ~(page_size - 1);
411 log.err("memory address 0x{x} leaked: {}", .{416 const addr = page_addr + slot_index * size_class;
412 @intFromPtr(addr), stack_trace,417 log.err("memory address 0x{x} leaked: {}", .{ addr, stack_trace });
413 });
414 leaks = true;418 leaks = true;
415 }419 }
416 if (bit_index == math.maxInt(u3))420 if (bit_index == math.maxInt(u3))
...@@ -425,16 +429,16 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {...@@ -425,16 +429,16 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {
425 pub fn detectLeaks(self: *Self) bool {429 pub fn detectLeaks(self: *Self) bool {
426 var leaks = false;430 var leaks = false;
427431
428 for (&self.buckets, 0..) |*buckets, bucket_i| {432 for (self.buckets, 0..) |init_optional_bucket, size_class_index| {
429 if (buckets.root == null) continue;433 var optional_bucket = init_optional_bucket;
430 const size_class = @as(usize, 1) << @as(math.Log2Int(usize), @intCast(bucket_i));434 const slot_count = slot_counts[size_class_index];
431 const used_bits_count = usedBitsCount(size_class);435 const used_bits_count = usedBitsCount(slot_count);
432 var it = buckets.inorderIterator();436 while (optional_bucket) |bucket| {
433 while (it.next()) |node| {437 leaks = detectLeaksInBucket(bucket, size_class_index, used_bits_count) or leaks;
434 const bucket = node.key;438 optional_bucket = bucket.prev;
435 leaks = detectLeaksInBucket(bucket, size_class, used_bits_count) or leaks;
436 }439 }
437 }440 }
441
438 var it = self.large_allocations.valueIterator();442 var it = self.large_allocations.valueIterator();
439 while (it.next()) |large_alloc| {443 while (it.next()) |large_alloc| {
440 if (config.retain_metadata and large_alloc.freed) continue;444 if (config.retain_metadata and large_alloc.freed) continue;
...@@ -447,46 +451,21 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {...@@ -447,46 +451,21 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {
447 return leaks;451 return leaks;
448 }452 }
449453
450 fn freeBucket(self: *Self, bucket: *BucketHeader, size_class: usize) void {
451 const bucket_size = bucketSize(size_class);
452 const bucket_slice = @as([*]align(@alignOf(BucketHeader)) u8, @ptrCast(bucket))[0..bucket_size];
453 self.backing_allocator.free(bucket_slice);
454 }
455
456 fn freeRetainedMetadata(self: *Self) void {454 fn freeRetainedMetadata(self: *Self) void {
457 if (config.retain_metadata) {455 comptime assert(config.retain_metadata);
458 if (config.never_unmap) {456 if (config.never_unmap) {
459 // free large allocations that were intentionally leaked by never_unmap457 // free large allocations that were intentionally leaked by never_unmap
460 var it = self.large_allocations.iterator();458 var it = self.large_allocations.iterator();
461 while (it.next()) |large| {459 while (it.next()) |large| {
462 if (large.value_ptr.freed) {460 if (large.value_ptr.freed) {
463 self.backing_allocator.rawFree(large.value_ptr.bytes, large.value_ptr.alignment, @returnAddress());461 self.backing_allocator.rawFree(large.value_ptr.bytes, large.value_ptr.alignment, @returnAddress());
464 }
465 }
466 }
467 // free retained metadata for small allocations
468 while (self.empty_buckets.getMin()) |node| {
469 // remove the node from the tree before destroying it
470 var entry = self.empty_buckets.getEntryForExisting(node);
471 entry.set(null);
472
473 var bucket = node.key;
474 if (config.never_unmap) {
475 // free page that was intentionally leaked by never_unmap
476 self.backing_allocator.free(bucket.page[0..page_size]);
477 }462 }
478 // alloc_cursor was set to slot count when bucket added to empty_buckets
479 self.freeBucket(bucket, bucket.emptyBucketSizeClass());
480 self.bucket_node_pool.destroy(node);
481 }463 }
482 self.empty_buckets.root = null;
483 }464 }
484 }465 }
485466
486 pub fn flushRetainedMetadata(self: *Self) void {467 pub fn flushRetainedMetadata(self: *Self) void {
487 if (!config.retain_metadata) {468 comptime assert(config.retain_metadata);
488 @compileError("'flushRetainedMetadata' requires 'config.retain_metadata = true'");
489 }
490 self.freeRetainedMetadata();469 self.freeRetainedMetadata();
491 // also remove entries from large_allocations470 // also remove entries from large_allocations
492 var it = self.large_allocations.iterator();471 var it = self.large_allocations.iterator();
...@@ -500,13 +479,10 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {...@@ -500,13 +479,10 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {
500 /// Returns `Check.leak` if there were leaks; `Check.ok` otherwise.479 /// Returns `Check.leak` if there were leaks; `Check.ok` otherwise.
501 pub fn deinit(self: *Self) Check {480 pub fn deinit(self: *Self) Check {
502 const leaks = if (config.safety) self.detectLeaks() else false;481 const leaks = if (config.safety) self.detectLeaks() else false;
503 if (config.retain_metadata) {482 if (config.retain_metadata) self.freeRetainedMetadata();
504 self.freeRetainedMetadata();
505 }
506 self.large_allocations.deinit(self.backing_allocator);483 self.large_allocations.deinit(self.backing_allocator);
507 self.bucket_node_pool.deinit();
508 self.* = undefined;484 self.* = undefined;
509 return @as(Check, @enumFromInt(@intFromBool(leaks)));485 return if (leaks) .leak else .ok;
510 }486 }
511487
512 fn collectStackTrace(first_trace_addr: usize, addresses: *[stack_n]usize) void {488 fn collectStackTrace(first_trace_addr: usize, addresses: *[stack_n]usize) void {
...@@ -520,8 +496,8 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {...@@ -520,8 +496,8 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {
520 }496 }
521497
522 fn reportDoubleFree(ret_addr: usize, alloc_stack_trace: StackTrace, free_stack_trace: StackTrace) void {498 fn reportDoubleFree(ret_addr: usize, alloc_stack_trace: StackTrace, free_stack_trace: StackTrace) void {
523 var addresses: [stack_n]usize = [1]usize{0} ** stack_n;499 var addresses: [stack_n]usize = @splat(0);
524 var second_free_stack_trace = StackTrace{500 var second_free_stack_trace: StackTrace = .{
525 .instruction_addresses = &addresses,501 .instruction_addresses = &addresses,
526 .index = 0,502 .index = 0,
527 };503 };
...@@ -531,58 +507,6 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {...@@ -531,58 +507,6 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {
531 });507 });
532 }508 }
533509
534 const Slot = struct {
535 bucket: *BucketHeader,
536 slot_index: usize,
537 ptr: [*]u8,
538 };
539
540 fn allocSlot(self: *Self, size_class: usize, trace_addr: usize) Error!Slot {
541 const bucket_index = math.log2(size_class);
542 var buckets = &self.buckets[bucket_index];
543 const slot_count = @divExact(page_size, size_class);
544 if (self.cur_buckets[bucket_index] == null or self.cur_buckets[bucket_index].?.alloc_cursor == slot_count) {
545 const new_bucket = try self.createBucket(size_class);
546 errdefer self.freeBucket(new_bucket, size_class);
547 const node = try self.bucket_node_pool.create();
548 node.key = new_bucket;
549 var entry = buckets.getEntryFor(new_bucket);
550 std.debug.assert(entry.node == null);
551 entry.set(node);
552 self.cur_buckets[bucket_index] = node.key;
553 }
554 const bucket = self.cur_buckets[bucket_index].?;
555
556 const slot_index = bucket.alloc_cursor;
557 bucket.alloc_cursor += 1;
558
559 const used_bits_byte = bucket.usedBits(slot_index / 8);
560 const used_bit_index: u3 = @as(u3, @intCast(slot_index % 8)); // TODO cast should be unnecessary
561 used_bits_byte.* |= (@as(u8, 1) << used_bit_index);
562 bucket.used_count += 1;
563 bucket.captureStackTrace(trace_addr, size_class, slot_index, .alloc);
564 return .{
565 .bucket = bucket,
566 .slot_index = slot_index,
567 .ptr = bucket.page + slot_index * size_class,
568 };
569 }
570
571 fn searchBucket(
572 buckets: *Buckets,
573 addr: usize,
574 current_bucket: ?*BucketHeader,
575 ) ?*BucketHeader {
576 const search_page: [*]align(page_size) u8 = @ptrFromInt(mem.alignBackward(usize, addr, page_size));
577 if (current_bucket != null and current_bucket.?.page == search_page) {
578 return current_bucket;
579 }
580 var search_header: BucketHeader = undefined;
581 search_header.page = search_page;
582 const entry = buckets.getEntryFor(&search_header);
583 return if (entry.node) |node| node.key else null;
584 }
585
586 /// This function assumes the object is in the large object storage regardless510 /// This function assumes the object is in the large object storage regardless
587 /// of the parameters.511 /// of the parameters.
588 fn resizeLarge(512 fn resizeLarge(
...@@ -752,201 +676,177 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {...@@ -752,201 +676,177 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {
752 }676 }
753 }677 }
754678
755 pub fn setRequestedMemoryLimit(self: *Self, limit: usize) void {679 fn alloc(context: *anyopaque, len: usize, alignment: mem.Alignment, ret_addr: usize) ?[*]u8 {
756 self.requested_memory_limit = limit;
757 }
758
759 fn resize(
760 context: *anyopaque,
761 memory: []u8,
762 alignment: mem.Alignment,
763 new_len: usize,
764 return_address: usize,
765 ) bool {
766 return realloc(context, memory, alignment, new_len, return_address, false) != null;
767 }
768
769 fn remap(
770 context: *anyopaque,
771 memory: []u8,
772 alignment: mem.Alignment,
773 new_len: usize,
774 return_address: usize,
775 ) ?[*]u8 {
776 return realloc(context, memory, alignment, new_len, return_address, true);
777 }
778
779 fn realloc(
780 context: *anyopaque,
781 old_mem: []u8,
782 alignment: mem.Alignment,
783 new_len: usize,
784 ret_addr: usize,
785 may_move: bool,
786 ) ?[*]u8 {
787 const self: *Self = @ptrCast(@alignCast(context));680 const self: *Self = @ptrCast(@alignCast(context));
788 self.mutex.lock();681 self.mutex.lock();
789 defer self.mutex.unlock();682 defer self.mutex.unlock();
790683
791 assert(old_mem.len != 0);684 if (config.enable_memory_limit) {
792685 const new_req_bytes = self.total_requested_bytes + len;
793 const aligned_size = @max(old_mem.len, alignment.toByteUnits());686 if (new_req_bytes > self.requested_memory_limit) return null;
794 if (aligned_size > largest_bucket_object_size) {687 self.total_requested_bytes = new_req_bytes;
795 return self.resizeLarge(old_mem, alignment, new_len, ret_addr, may_move);
796 }688 }
797 const size_class_hint = math.ceilPowerOfTwoAssert(usize, aligned_size);
798689
799 var bucket_index = math.log2(size_class_hint);690 const size_class_index: usize = @max(@bitSizeOf(usize) - @clz(len - 1), @intFromEnum(alignment));
800 var size_class: usize = size_class_hint;691 if (size_class_index >= self.buckets.len) {
801 const bucket = while (bucket_index < small_bucket_count) : (bucket_index += 1) {692 @branchHint(.unlikely);
802 if (searchBucket(&self.buckets[bucket_index], @intFromPtr(old_mem.ptr), self.cur_buckets[bucket_index])) |bucket| {693 self.large_allocations.ensureUnusedCapacity(self.backing_allocator, 1) catch return null;
803 break bucket;694 const ptr = self.backing_allocator.rawAlloc(len, alignment, ret_addr) orelse return null;
695 const slice = ptr[0..len];
696
697 const gop = self.large_allocations.getOrPutAssumeCapacity(@intFromPtr(slice.ptr));
698 if (config.retain_metadata and !config.never_unmap) {
699 // Backing allocator may be reusing memory that we're retaining metadata for
700 assert(!gop.found_existing or gop.value_ptr.freed);
701 } else {
702 assert(!gop.found_existing); // This would mean the kernel double-mapped pages.
804 }703 }
805 size_class *= 2;704 gop.value_ptr.bytes = slice;
806 } else blk: {705 if (config.enable_memory_limit)
706 gop.value_ptr.requested_size = len;
707 gop.value_ptr.captureStackTrace(ret_addr, .alloc);
807 if (config.retain_metadata) {708 if (config.retain_metadata) {
808 if (!self.large_allocations.contains(@intFromPtr(old_mem.ptr))) {709 gop.value_ptr.freed = false;
809 // object not in active buckets or a large allocation, so search empty buckets710 if (config.never_unmap) {
810 if (searchBucket(&self.empty_buckets, @intFromPtr(old_mem.ptr), null)) |bucket| {711 gop.value_ptr.alignment = alignment;
811 size_class = bucket.emptyBucketSizeClass();
812 // bucket is empty so is_used below will always be false and we exit there
813 break :blk bucket;
814 } else {
815 @panic("Invalid free");
816 }
817 }712 }
818 }713 }
819 return self.resizeLarge(old_mem, alignment, new_len, ret_addr, may_move);714
820 };715 if (config.verbose_log) {
821 const byte_offset = @intFromPtr(old_mem.ptr) - @intFromPtr(bucket.page);716 log.info("large alloc {d} bytes at {*}", .{ slice.len, slice.ptr });
822 const slot_index = @as(SlotIndex, @intCast(byte_offset / size_class));
823 const used_byte_index = slot_index / 8;
824 const used_bit_index = @as(u3, @intCast(slot_index % 8));
825 const used_byte = bucket.usedBits(used_byte_index);
826 const is_used = @as(u1, @truncate(used_byte.* >> used_bit_index)) != 0;
827 if (!is_used) {
828 if (config.safety) {
829 reportDoubleFree(ret_addr, bucketStackTrace(bucket, size_class, slot_index, .alloc), bucketStackTrace(bucket, size_class, slot_index, .free));
830 @panic("Unrecoverable double free");
831 } else {
832 unreachable;
833 }717 }
718 return slice.ptr;
834 }719 }
835720
836 // Definitely an in-use small alloc now.721 const slot_count = slot_counts[size_class_index];
837 if (config.safety) {722
838 const requested_size = bucket.requestedSizes(size_class)[slot_index];723 if (self.buckets[size_class_index]) |bucket| {
839 if (requested_size == 0) @panic("Invalid free");724 @branchHint(.likely);
840 const slot_alignment = bucket.log2PtrAligns(size_class)[slot_index];725 const slot_index = bucket.allocated_count;
841 if (old_mem.len != requested_size or alignment != slot_alignment) {726 if (slot_index < slot_count) {
842 var addresses: [stack_n]usize = [1]usize{0} ** stack_n;727 @branchHint(.likely);
843 var free_stack_trace = StackTrace{728 bucket.allocated_count = slot_index + 1;
844 .instruction_addresses = &addresses,729 const used_bits_byte = bucket.usedBits(slot_index / 8);
845 .index = 0,730 const used_bit_index: u3 = @intCast(slot_index % 8);
846 };731 used_bits_byte.* |= (@as(u8, 1) << used_bit_index);
847 std.debug.captureStackTrace(ret_addr, &free_stack_trace);732 const size_class = @as(usize, 1) << @as(u6, @intCast(size_class_index));
848 if (old_mem.len != requested_size) {733 if (config.stack_trace_frames > 0) {
849 log.err("Allocation size {d} bytes does not match resize size {d}. Allocation: {} Resize: {}", .{734 bucket.captureStackTrace(ret_addr, slot_count, slot_index, .alloc);
850 requested_size,
851 old_mem.len,
852 bucketStackTrace(bucket, size_class, slot_index, .alloc),
853 free_stack_trace,
854 });
855 }735 }
856 if (alignment != slot_alignment) {736 if (config.safety) {
857 log.err("Allocation alignment {d} does not match resize alignment {d}. Allocation: {} Resize: {}", .{737 bucket.requestedSizes(slot_count)[slot_index] = @intCast(len);
858 slot_alignment.toByteUnits(),738 bucket.log2PtrAligns(slot_count)[slot_index] = alignment;
859 alignment.toByteUnits(),
860 bucketStackTrace(bucket, size_class, slot_index, .alloc),
861 free_stack_trace,
862 });
863 }739 }
740 const page_addr = @intFromPtr(bucket) & ~(page_size - 1);
741 const addr = page_addr + slot_index * size_class;
742 if (config.verbose_log) {
743 log.info("small alloc {d} bytes at 0x{x}", .{ len, addr });
744 }
745 return @ptrFromInt(addr);
864 }746 }
865 }747 }
866 const prev_req_bytes = self.total_requested_bytes;748
867 if (config.enable_memory_limit) {749 const page = self.backing_allocator.rawAlloc(page_size, page_align, @returnAddress()) orelse
868 const new_req_bytes = prev_req_bytes + new_len - old_mem.len;750 return null;
869 if (new_req_bytes > prev_req_bytes and new_req_bytes > self.requested_memory_limit) {751 const bucket: *BucketHeader = .fromPage(@intFromPtr(page), slot_count);
870 return null;752 bucket.* = .{
871 }753 .allocated_count = 1,
872 self.total_requested_bytes = new_req_bytes;754 .freed_count = 0,
755 .prev = self.buckets[size_class_index],
756 };
757 self.buckets[size_class_index] = bucket;
758
759 if (!config.backing_allocator_zeroes) {
760 @memset(@as([*]u8, @as(*[1]u8, bucket.usedBits(0)))[0..usedBitsCount(slot_count)], 0);
761 if (config.safety) @memset(bucket.requestedSizes(slot_count), 0);
873 }762 }
874763
875 const new_aligned_size = @max(new_len, alignment.toByteUnits());764 bucket.usedBits(0).* = 0b1;
876 const new_size_class = math.ceilPowerOfTwoAssert(usize, new_aligned_size);765
877 if (new_size_class <= size_class) {766 if (config.stack_trace_frames > 0) {
878 if (old_mem.len > new_len) {767 bucket.captureStackTrace(ret_addr, slot_count, 0, .alloc);
879 @memset(old_mem[new_len..], undefined);
880 }
881 if (config.verbose_log) {
882 log.info("small resize {d} bytes at {*} to {d}", .{
883 old_mem.len, old_mem.ptr, new_len,
884 });
885 }
886 if (config.safety) {
887 bucket.requestedSizes(size_class)[slot_index] = @intCast(new_len);
888 }
889 return old_mem.ptr;
890 }768 }
891769
892 if (config.enable_memory_limit) {770 if (config.safety) {
893 self.total_requested_bytes = prev_req_bytes;771 bucket.requestedSizes(slot_count)[0] = @intCast(len);
772 bucket.log2PtrAligns(slot_count)[0] = alignment;
894 }773 }
774
775 if (config.verbose_log) {
776 log.info("small alloc {d} bytes at 0x{x}", .{ len, @intFromPtr(page) });
777 }
778
779 return page;
780 }
781
782 fn resize(
783 context: *anyopaque,
784 memory: []u8,
785 alignment: mem.Alignment,
786 new_len: usize,
787 return_address: usize,
788 ) bool {
789 _ = context;
790 _ = memory;
791 _ = alignment;
792 _ = new_len;
793 _ = return_address;
794 return false;
795 }
796
797 fn remap(
798 context: *anyopaque,
799 memory: []u8,
800 alignment: mem.Alignment,
801 new_len: usize,
802 return_address: usize,
803 ) ?[*]u8 {
804 _ = context;
805 _ = memory;
806 _ = alignment;
807 _ = new_len;
808 _ = return_address;
895 return null;809 return null;
896 }810 }
897811
898 fn free(812 fn free(
899 ctx: *anyopaque,813 context: *anyopaque,
900 old_mem: []u8,814 old_memory: []u8,
901 alignment: mem.Alignment,815 alignment: mem.Alignment,
902 ret_addr: usize,816 return_address: usize,
903 ) void {817 ) void {
904 const self: *Self = @ptrCast(@alignCast(ctx));818 const self: *Self = @ptrCast(@alignCast(context));
905 self.mutex.lock();819 self.mutex.lock();
906 defer self.mutex.unlock();820 defer self.mutex.unlock();
907821
908 assert(old_mem.len != 0);822 assert(old_memory.len != 0);
909823
910 const aligned_size = @max(old_mem.len, alignment.toByteUnits());824 const size_class_index: usize = @max(@bitSizeOf(usize) - @clz(old_memory.len - 1), @intFromEnum(alignment));
911 if (aligned_size > largest_bucket_object_size) {825 if (size_class_index >= self.buckets.len) {
912 self.freeLarge(old_mem, alignment, ret_addr);826 @branchHint(.unlikely);
827 self.freeLarge(old_memory, alignment, return_address);
913 return;828 return;
914 }829 }
915 const size_class_hint = math.ceilPowerOfTwoAssert(usize, aligned_size);
916830
917 var bucket_index = math.log2(size_class_hint);831 const slot_count = slot_counts[size_class_index];
918 var size_class: usize = size_class_hint;832 const freed_addr = @intFromPtr(old_memory.ptr);
919 const bucket = while (bucket_index < small_bucket_count) : (bucket_index += 1) {833 const page_addr = freed_addr & ~(page_size - 1);
920 if (searchBucket(&self.buckets[bucket_index], @intFromPtr(old_mem.ptr), self.cur_buckets[bucket_index])) |bucket| {834 const bucket: *BucketHeader = .fromPage(page_addr, slot_count);
921 break bucket;835 const page_offset = freed_addr - page_addr;
922 }836 const size_class = @as(usize, 1) << @as(u6, @intCast(size_class_index));
923 size_class *= 2;837 const slot_index: SlotIndex = @intCast(page_offset / size_class);
924 } else blk: {
925 if (config.retain_metadata) {
926 if (!self.large_allocations.contains(@intFromPtr(old_mem.ptr))) {
927 // object not in active buckets or a large allocation, so search empty buckets
928 if (searchBucket(&self.empty_buckets, @intFromPtr(old_mem.ptr), null)) |bucket| {
929 size_class = bucket.emptyBucketSizeClass();
930 // bucket is empty so is_used below will always be false and we exit there
931 break :blk bucket;
932 } else {
933 @panic("Invalid free");
934 }
935 }
936 }
937 self.freeLarge(old_mem, alignment, ret_addr);
938 return;
939 };
940 const byte_offset = @intFromPtr(old_mem.ptr) - @intFromPtr(bucket.page);
941 const slot_index = @as(SlotIndex, @intCast(byte_offset / size_class));
942 const used_byte_index = slot_index / 8;838 const used_byte_index = slot_index / 8;
943 const used_bit_index = @as(u3, @intCast(slot_index % 8));839 const used_bit_index: u3 = @intCast(slot_index % 8);
944 const used_byte = bucket.usedBits(used_byte_index);840 const used_byte = bucket.usedBits(used_byte_index);
945 const is_used = @as(u1, @truncate(used_byte.* >> used_bit_index)) != 0;841 const is_used = @as(u1, @truncate(used_byte.* >> used_bit_index)) != 0;
946 if (!is_used) {842 if (!is_used) {
947 if (config.safety) {843 if (config.safety) {
948 reportDoubleFree(ret_addr, bucketStackTrace(bucket, size_class, slot_index, .alloc), bucketStackTrace(bucket, size_class, slot_index, .free));844 reportDoubleFree(
949 // Recoverable if this is a free.845 return_address,
846 bucketStackTrace(bucket, slot_count, slot_index, .alloc),
847 bucketStackTrace(bucket, slot_count, slot_index, .free),
848 );
849 // Recoverable since this is a free.
950 return;850 return;
951 } else {851 } else {
952 unreachable;852 unreachable;
...@@ -955,21 +855,21 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {...@@ -955,21 +855,21 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {
955855
956 // Definitely an in-use small alloc now.856 // Definitely an in-use small alloc now.
957 if (config.safety) {857 if (config.safety) {
958 const requested_size = bucket.requestedSizes(size_class)[slot_index];858 const requested_size = bucket.requestedSizes(slot_count)[slot_index];
959 if (requested_size == 0) @panic("Invalid free");859 if (requested_size == 0) @panic("Invalid free");
960 const slot_alignment = bucket.log2PtrAligns(size_class)[slot_index];860 const slot_alignment = bucket.log2PtrAligns(slot_count)[slot_index];
961 if (old_mem.len != requested_size or alignment != slot_alignment) {861 if (old_memory.len != requested_size or alignment != slot_alignment) {
962 var addresses: [stack_n]usize = [1]usize{0} ** stack_n;862 var addresses: [stack_n]usize = [1]usize{0} ** stack_n;
963 var free_stack_trace = StackTrace{863 var free_stack_trace: StackTrace = .{
964 .instruction_addresses = &addresses,864 .instruction_addresses = &addresses,
965 .index = 0,865 .index = 0,
966 };866 };
967 std.debug.captureStackTrace(ret_addr, &free_stack_trace);867 std.debug.captureStackTrace(return_address, &free_stack_trace);
968 if (old_mem.len != requested_size) {868 if (old_memory.len != requested_size) {
969 log.err("Allocation size {d} bytes does not match free size {d}. Allocation: {} Free: {}", .{869 log.err("Allocation size {d} bytes does not match free size {d}. Allocation: {} Free: {}", .{
970 requested_size,870 requested_size,
971 old_mem.len,871 old_memory.len,
972 bucketStackTrace(bucket, size_class, slot_index, .alloc),872 bucketStackTrace(bucket, slot_count, slot_index, .alloc),
973 free_stack_trace,873 free_stack_trace,
974 });874 });
975 }875 }
...@@ -977,7 +877,7 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {...@@ -977,7 +877,7 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {
977 log.err("Allocation alignment {d} does not match free alignment {d}. Allocation: {} Free: {}", .{877 log.err("Allocation alignment {d} does not match free alignment {d}. Allocation: {} Free: {}", .{
978 slot_alignment.toByteUnits(),878 slot_alignment.toByteUnits(),
979 alignment.toByteUnits(),879 alignment.toByteUnits(),
980 bucketStackTrace(bucket, size_class, slot_index, .alloc),880 bucketStackTrace(bucket, slot_count, slot_index, .alloc),
981 free_stack_trace,881 free_stack_trace,
982 });882 });
983 }883 }
...@@ -985,142 +885,29 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {...@@ -985,142 +885,29 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {
985 }885 }
986886
987 if (config.enable_memory_limit) {887 if (config.enable_memory_limit) {
988 self.total_requested_bytes -= old_mem.len;888 self.total_requested_bytes -= old_memory.len;
989 }889 }
990890
991 // Capture stack trace to be the "first free", in case a double free happens.891 // Capture stack trace to be the "first free", in case a double free happens.
992 bucket.captureStackTrace(ret_addr, size_class, slot_index, .free);892 bucket.captureStackTrace(return_address, slot_count, slot_index, .free);
993893
994 used_byte.* &= ~(@as(u8, 1) << used_bit_index);894 used_byte.* &= ~(@as(u8, 1) << used_bit_index);
995 bucket.used_count -= 1;
996 if (config.safety) {895 if (config.safety) {
997 bucket.requestedSizes(size_class)[slot_index] = 0;896 bucket.requestedSizes(slot_count)[slot_index] = 0;
998 }897 }
999 if (bucket.used_count == 0) {898 bucket.freed_count += 1;
1000 var entry = self.buckets[bucket_index].getEntryFor(bucket);899 if (bucket.freed_count == bucket.allocated_count) {
1001 // save the node for destruction/insertion into in empty_buckets900 if (self.buckets[size_class_index] == bucket) {
1002 const node = entry.node.?;901 self.buckets[size_class_index] = null;
1003 entry.set(null);
1004 if (self.cur_buckets[bucket_index] == bucket) {
1005 self.cur_buckets[bucket_index] = null;
1006 }902 }
1007 if (!config.never_unmap) {903 if (!config.never_unmap) {
1008 self.backing_allocator.free(bucket.page[0..page_size]);904 const page: [*]align(page_size) u8 = @ptrFromInt(page_addr);
1009 }905 self.backing_allocator.rawFree(page[0..page_size], page_align, @returnAddress());
1010 if (!config.retain_metadata) {
1011 self.freeBucket(bucket, size_class);
1012 self.bucket_node_pool.destroy(node);
1013 } else {
1014 // move alloc_cursor to end so we can tell size_class later
1015 const slot_count = @divExact(page_size, size_class);
1016 bucket.alloc_cursor = @as(SlotIndex, @truncate(slot_count));
1017 var empty_entry = self.empty_buckets.getEntryFor(node.key);
1018 empty_entry.set(node);
1019 }
1020 } else {
1021 @memset(old_mem, undefined);
1022 }
1023 if (config.verbose_log) {
1024 log.info("small free {d} bytes at {*}", .{ old_mem.len, old_mem.ptr });
1025 }
1026 }
1027
1028 // Returns true if an allocation of `size` bytes is within the specified
1029 // limits if enable_memory_limit is true
1030 fn isAllocationAllowed(self: *Self, size: usize) bool {
1031 if (config.enable_memory_limit) {
1032 const new_req_bytes = self.total_requested_bytes + size;
1033 if (new_req_bytes > self.requested_memory_limit)
1034 return false;
1035 self.total_requested_bytes = new_req_bytes;
1036 }
1037
1038 return true;
1039 }
1040
1041 fn alloc(ctx: *anyopaque, len: usize, alignment: mem.Alignment, ret_addr: usize) ?[*]u8 {
1042 const self: *Self = @ptrCast(@alignCast(ctx));
1043 self.mutex.lock();
1044 defer self.mutex.unlock();
1045 if (!self.isAllocationAllowed(len)) return null;
1046 return allocInner(self, len, alignment, ret_addr) catch return null;
1047 }
1048
1049 fn allocInner(
1050 self: *Self,
1051 len: usize,
1052 alignment: mem.Alignment,
1053 ret_addr: usize,
1054 ) Allocator.Error![*]u8 {
1055 const new_aligned_size = @max(len, alignment.toByteUnits());
1056 if (new_aligned_size > largest_bucket_object_size) {
1057 try self.large_allocations.ensureUnusedCapacity(self.backing_allocator, 1);
1058 const ptr = self.backing_allocator.rawAlloc(len, alignment, ret_addr) orelse
1059 return error.OutOfMemory;
1060 const slice = ptr[0..len];
1061
1062 const gop = self.large_allocations.getOrPutAssumeCapacity(@intFromPtr(slice.ptr));
1063 if (config.retain_metadata and !config.never_unmap) {
1064 // Backing allocator may be reusing memory that we're retaining metadata for
1065 assert(!gop.found_existing or gop.value_ptr.freed);
1066 } else {
1067 assert(!gop.found_existing); // This would mean the kernel double-mapped pages.
1068 }906 }
1069 gop.value_ptr.bytes = slice;
1070 if (config.enable_memory_limit)
1071 gop.value_ptr.requested_size = len;
1072 gop.value_ptr.captureStackTrace(ret_addr, .alloc);
1073 if (config.retain_metadata) {
1074 gop.value_ptr.freed = false;
1075 if (config.never_unmap) {
1076 gop.value_ptr.alignment = alignment;
1077 }
1078 }
1079
1080 if (config.verbose_log) {
1081 log.info("large alloc {d} bytes at {*}", .{ slice.len, slice.ptr });
1082 }
1083 return slice.ptr;
1084 }
1085
1086 const new_size_class = math.ceilPowerOfTwoAssert(usize, new_aligned_size);
1087 const slot = try self.allocSlot(new_size_class, ret_addr);
1088 if (config.safety) {
1089 slot.bucket.requestedSizes(new_size_class)[slot.slot_index] = @intCast(len);
1090 slot.bucket.log2PtrAligns(new_size_class)[slot.slot_index] = alignment;
1091 }907 }
1092 if (config.verbose_log) {908 if (config.verbose_log) {
1093 log.info("small alloc {d} bytes at {*}", .{ len, slot.ptr });909 log.info("small free {d} bytes at {*}", .{ old_memory.len, old_memory.ptr });
1094 }
1095 return slot.ptr;
1096 }
1097
1098 fn createBucket(self: *Self, size_class: usize) Error!*BucketHeader {
1099 const alignment: mem.Alignment = .fromByteUnits(page_size);
1100 const page = self.backing_allocator.rawAlloc(page_size, alignment, @returnAddress()) orelse
1101 return error.OutOfMemory;
1102 errdefer self.backing_allocator.rawFree(page[0..page_size], alignment, @returnAddress());
1103
1104 const bucket_size = bucketSize(size_class);
1105 const header_align: mem.Alignment = .fromByteUnits(@alignOf(BucketHeader));
1106 const ptr: *BucketHeader = @alignCast(@ptrCast(self.backing_allocator.rawAlloc(
1107 bucket_size,
1108 header_align,
1109 @returnAddress(),
1110 ) orelse return error.OutOfMemory));
1111 ptr.* = .{
1112 .page = @alignCast(page),
1113 .alloc_cursor = 0,
1114 .used_count = 0,
1115 };
1116 if (!config.backing_allocator_zeroes) {
1117 @memset(@as([*]u8, @as(*[1]u8, ptr.usedBits(0)))[0..usedBitsCount(size_class)], 0);
1118 if (config.safety) {
1119 // Set the requested sizes to zeroes
1120 @memset(mem.sliceAsBytes(ptr.requestedSizes(size_class)), 0);
1121 }
1122 }910 }
1123 return ptr;
1124 }911 }
1125 };912 };
1126}913}
...@@ -1460,7 +1247,7 @@ test "setting a memory cap" {...@@ -1460,7 +1247,7 @@ test "setting a memory cap" {
1460 defer std.testing.expect(gpa.deinit() == .ok) catch @panic("leak");1247 defer std.testing.expect(gpa.deinit() == .ok) catch @panic("leak");
1461 const allocator = gpa.allocator();1248 const allocator = gpa.allocator();
14621249
1463 gpa.setRequestedMemoryLimit(1010);1250 gpa.requested_memory_limit = 1010;
14641251
1465 const small = try allocator.create(i32);1252 const small = try allocator.create(i32);
1466 try std.testing.expect(gpa.total_requested_bytes == 4);1253 try std.testing.expect(gpa.total_requested_bytes == 4);
...@@ -1481,63 +1268,6 @@ test "setting a memory cap" {...@@ -1481,63 +1268,6 @@ test "setting a memory cap" {
1481 allocator.free(exact);1268 allocator.free(exact);
1482}1269}
14831270
1484test "double frees" {
1485 // use a GPA to back a GPA to check for leaks of the latter's metadata
1486 var backing_gpa = GeneralPurposeAllocator(.{ .safety = true }){};
1487 defer std.testing.expect(backing_gpa.deinit() == .ok) catch @panic("leak");
1488
1489 const GPA = GeneralPurposeAllocator(.{ .safety = true, .never_unmap = true, .retain_metadata = true });
1490 var gpa = GPA{ .backing_allocator = backing_gpa.allocator() };
1491 defer std.testing.expect(gpa.deinit() == .ok) catch @panic("leak");
1492 const allocator = gpa.allocator();
1493
1494 // detect a small allocation double free, even though bucket is emptied
1495 const index: usize = 6;
1496 const size_class: usize = @as(usize, 1) << 6;
1497 const small = try allocator.alloc(u8, size_class);
1498 try std.testing.expect(GPA.searchBucket(&gpa.buckets[index], @intFromPtr(small.ptr), gpa.cur_buckets[index]) != null);
1499 allocator.free(small);
1500 try std.testing.expect(GPA.searchBucket(&gpa.buckets[index], @intFromPtr(small.ptr), gpa.cur_buckets[index]) == null);
1501 try std.testing.expect(GPA.searchBucket(&gpa.empty_buckets, @intFromPtr(small.ptr), null) != null);
1502
1503 // detect a large allocation double free
1504 const large = try allocator.alloc(u8, 2 * page_size);
1505 try std.testing.expect(gpa.large_allocations.contains(@intFromPtr(large.ptr)));
1506 try std.testing.expectEqual(gpa.large_allocations.getEntry(@intFromPtr(large.ptr)).?.value_ptr.bytes, large);
1507 allocator.free(large);
1508 try std.testing.expect(gpa.large_allocations.contains(@intFromPtr(large.ptr)));
1509 try std.testing.expect(gpa.large_allocations.getEntry(@intFromPtr(large.ptr)).?.value_ptr.freed);
1510
1511 const normal_small = try allocator.alloc(u8, size_class);
1512 defer allocator.free(normal_small);
1513 const normal_large = try allocator.alloc(u8, 2 * page_size);
1514 defer allocator.free(normal_large);
1515
1516 // check that flushing retained metadata doesn't disturb live allocations
1517 gpa.flushRetainedMetadata();
1518 try std.testing.expect(gpa.empty_buckets.root == null);
1519 try std.testing.expect(GPA.searchBucket(&gpa.buckets[index], @intFromPtr(normal_small.ptr), gpa.cur_buckets[index]) != null);
1520 try std.testing.expect(gpa.large_allocations.contains(@intFromPtr(normal_large.ptr)));
1521 try std.testing.expect(!gpa.large_allocations.contains(@intFromPtr(large.ptr)));
1522}
1523
1524test "empty bucket size class" {
1525 const GPA = GeneralPurposeAllocator(.{ .safety = true, .never_unmap = true, .retain_metadata = true });
1526 var gpa = GPA{};
1527 defer std.testing.expect(gpa.deinit() == .ok) catch @panic("leak");
1528 const allocator = gpa.allocator();
1529
1530 // allocate and free to create an empty bucket
1531 const size_class: usize = @as(usize, 1) << 6;
1532 const small = try allocator.alloc(u8, size_class);
1533 allocator.free(small);
1534
1535 // the metadata tracking system relies on alloc_cursor of empty buckets
1536 // being set to the slot count so that we can get back the size class.
1537 const empty_bucket = GPA.searchBucket(&gpa.empty_buckets, @intFromPtr(small.ptr), null).?;
1538 try std.testing.expect(empty_bucket.emptyBucketSizeClass() == size_class);
1539}
1540
1541test "bug 9995 fix, large allocs count requested size not backing size" {1271test "bug 9995 fix, large allocs count requested size not backing size" {
1542 // with AtLeast, buffer likely to be larger than requested, especially when shrinking1272 // with AtLeast, buffer likely to be larger than requested, especially when shrinking
1543 var gpa = GeneralPurposeAllocator(.{ .enable_memory_limit = true }){};1273 var gpa = GeneralPurposeAllocator(.{ .enable_memory_limit = true }){};