//! # General Purpose Allocator //! //! ## Design Priorities //! //! ### `OptimizationMode.debug` and `OptimizationMode.release_safe`: //! //! * Detect double free, and emit stack trace of: //! - Where it was first allocated //! - Where it was freed the first time //! - Where it was freed the second time //! //! * Detect leaks and emit stack trace of: //! - Where it was allocated //! //! * When a page of memory is no longer needed, give it back to resident memory //! as soon as possible, so that it causes page faults when used. //! //! * Do not re-use memory slots, so that memory safety is upheld. For small //! allocations, this is handled here; for larger ones it is handled in the //! backing allocator (by default `std.heap.page_allocator`). //! //! * Make pointer math errors unlikely to harm memory from //! unrelated allocations. //! //! * It's OK for these mechanisms to cost some extra overhead bytes. //! //! * It's OK for performance cost for these mechanisms. //! //! * Rogue memory writes should not harm the allocator's state. //! //! * Cross platform. Operates based on a backing allocator which makes it work //! everywhere, even freestanding. //! //! * Compile-time configuration. //! //! ### `OptimizationMode.release_fast` (note: not much work has gone into this use case yet): //! //! * Low fragmentation is primary concern //! * Performance of worst-case latency is secondary concern //! * Performance of average-case latency is next //! * Finally, having freed memory unmapped, and pointer math errors unlikely to //! harm memory from unrelated allocations are nice-to-haves. //! //! ### `OptimizationMode.release_small` (note: not much work has gone into this use case yet): //! //! * Small binary code size of the executable is the primary concern. //! * Next, defer to the `.release_fast` priority list. //! //! ## Basic Design: //! //! Small allocations are divided into buckets: //! //! ``` //! index obj_size //! 0 1 //! 1 2 //! 2 4 //! 3 8 //! 4 16 //! 5 32 //! 6 64 //! 7 128 //! 8 256 //! 9 512 //! 10 1024 //! 11 2048 //! ``` //! //! The main allocator state has an array of all the "current" buckets for each //! size class. Each slot in the array can be null, meaning the bucket for that //! size class is not allocated. When the first object is allocated for a given //! size class, it allocates 1 page of memory from the OS. This page is //! divided into "slots" - one per allocated object. Along with the page of memory //! for object slots, as many pages as necessary are allocated to store the //! BucketHeader, followed by "used bits", and two stack traces for each slot //! (allocation trace and free trace). //! //! The "used bits" are 1 bit per slot representing whether the slot is used. //! Allocations use the data to iterate to find a free slot. Frees assert that the //! corresponding bit is 1 and set it to 0. //! //! Buckets have prev and next pointers. When there is only one bucket for a given //! size class, both prev and next point to itself. When all slots of a bucket are //! used, a new bucket is allocated, and enters the doubly linked list. The main //! allocator state tracks the "current" bucket for each size class. Leak detection //! currently only checks the current bucket. //! //! Resizing detects if the size class is unchanged or smaller, in which case the same //! pointer is returned unmodified. If a larger size class is required, //! `error.OutOfMemory` is returned. //! //! Large objects are allocated directly using the backing allocator and their metadata is stored //! in a `std.HashMap` using the backing allocator. const std = @import("std"); const builtin = @import("builtin"); const log = std.log.scoped(.gpa); const math = std.math; const assert = std.debug.assert; const mem = std.mem; const Allocator = std.mem.Allocator; const page_size = std.mem.page_size; const StackTrace = std.builtin.StackTrace; /// Integer type for pointing to slots in a small allocation const SlotIndex = std.meta.Int(.unsigned, math.log2(page_size) + 1); const default_test_stack_trace_frames: usize = if (builtin.is_test) 8 else 4; const default_sys_stack_trace_frames: usize = if (std.debug.sys_can_stack_trace) default_test_stack_trace_frames else 0; const default_stack_trace_frames: usize = switch (builtin.mode) { .Debug => default_sys_stack_trace_frames, else => 0, }; pub const Config = struct { /// Number of stack frames to capture. stack_trace_frames: usize = default_stack_trace_frames, /// If true, the allocator will have two fields: /// * `total_requested_bytes` which tracks the total allocated bytes of memory requested. /// * `requested_memory_limit` which causes allocations to return `error.OutOfMemory` /// when the `total_requested_bytes` exceeds this limit. /// If false, these fields will be `void`. enable_memory_limit: bool = false, /// Whether to enable safety checks. safety: bool = std.debug.runtime_safety, /// Whether the allocator may be used simultaneously from multiple threads. thread_safe: bool = !builtin.single_threaded, /// What type of mutex you'd like to use, for thread safety. /// when specified, the mutex type must have the same shape as `std.Thread.Mutex` and /// `DummyMutex`, and have no required fields. Specifying this field causes /// the `thread_safe` field to be ignored. /// /// when null (default): /// * the mutex type defaults to `std.Thread.Mutex` when thread_safe is enabled. /// * the mutex type defaults to `DummyMutex` otherwise. MutexType: ?type = null, /// This is a temporary debugging trick you can use to turn segfaults into more helpful /// logged error messages with stack trace details. The downside is that every allocation /// will be leaked, unless used with retain_metadata! never_unmap: bool = false, /// This is a temporary debugging aid that retains metadata about allocations indefinitely. /// This allows a greater range of double frees to be reported. All metadata is freed when /// deinit is called. When used with never_unmap, deliberately leaked memory is also freed /// during deinit. Currently should be used with never_unmap to avoid segfaults. /// TODO https://github.com/ziglang/zig/issues/4298 will allow use without never_unmap retain_metadata: bool = false, /// Enables emitting info messages with the size and address of every allocation. verbose_log: bool = false, }; pub const Check = enum { ok, leak }; pub fn GeneralPurposeAllocator(comptime config: Config) type { return struct { backing_allocator: Allocator = std.heap.page_allocator, buckets: [small_bucket_count]?*BucketHeader = [1]?*BucketHeader{null} ** small_bucket_count, large_allocations: LargeAllocTable = .{}, small_allocations: if (config.safety) SmallAllocTable else void = if (config.safety) .{} else {}, empty_buckets: if (config.retain_metadata) ?*BucketHeader else void = if (config.retain_metadata) null else {}, total_requested_bytes: @TypeOf(total_requested_bytes_init) = total_requested_bytes_init, requested_memory_limit: @TypeOf(requested_memory_limit_init) = requested_memory_limit_init, mutex: @TypeOf(mutex_init) = mutex_init, const Self = @This(); const total_requested_bytes_init = if (config.enable_memory_limit) @as(usize, 0) else {}; const requested_memory_limit_init = if (config.enable_memory_limit) @as(usize, math.maxInt(usize)) else {}; const mutex_init = if (config.MutexType) |T| T{} else if (config.thread_safe) std.Thread.Mutex{} else DummyMutex{}; const DummyMutex = struct { fn lock(_: *DummyMutex) void {} fn unlock(_: *DummyMutex) void {} }; const stack_n = config.stack_trace_frames; const one_trace_size = @sizeOf(usize) * stack_n; const traces_per_slot = 2; pub const Error = mem.Allocator.Error; const small_bucket_count = math.log2(page_size); const largest_bucket_object_size = 1 << (small_bucket_count - 1); const SmallAlloc = struct { requested_size: usize, log2_ptr_align: u8, }; const LargeAlloc = struct { bytes: []u8, requested_size: if (config.enable_memory_limit) usize else void, stack_addresses: [trace_n][stack_n]usize, freed: if (config.retain_metadata) bool else void, log2_ptr_align: if (config.never_unmap and config.retain_metadata) u8 else void, const trace_n = if (config.retain_metadata) traces_per_slot else 1; fn dumpStackTrace(self: *LargeAlloc, trace_kind: TraceKind) void { std.debug.dumpStackTrace(self.getStackTrace(trace_kind)); } fn getStackTrace(self: *LargeAlloc, trace_kind: TraceKind) std.builtin.StackTrace { assert(@intFromEnum(trace_kind) < trace_n); const stack_addresses = &self.stack_addresses[@intFromEnum(trace_kind)]; var len: usize = 0; while (len < stack_n and stack_addresses[len] != 0) { len += 1; } return .{ .instruction_addresses = stack_addresses, .index = len, }; } fn captureStackTrace(self: *LargeAlloc, ret_addr: usize, trace_kind: TraceKind) void { assert(@intFromEnum(trace_kind) < trace_n); const stack_addresses = &self.stack_addresses[@intFromEnum(trace_kind)]; collectStackTrace(ret_addr, stack_addresses); } }; const LargeAllocTable = std.AutoHashMapUnmanaged(usize, LargeAlloc); const SmallAllocTable = std.AutoHashMapUnmanaged(usize, SmallAlloc); // Bucket: In memory, in order: // * BucketHeader // * bucket_used_bits: [N]u8, // 1 bit for every slot; 1 byte for every 8 slots // * stack_trace_addresses: [N]usize, // traces_per_slot for every allocation const BucketHeader = struct { prev: *BucketHeader, next: *BucketHeader, page: [*]align(page_size) u8, alloc_cursor: SlotIndex, used_count: SlotIndex, fn usedBits(bucket: *BucketHeader, index: usize) *u8 { return @as(*u8, @ptrFromInt(@intFromPtr(bucket) + @sizeOf(BucketHeader) + index)); } fn stackTracePtr( bucket: *BucketHeader, size_class: usize, slot_index: SlotIndex, trace_kind: TraceKind, ) *[stack_n]usize { const start_ptr = @as([*]u8, @ptrCast(bucket)) + bucketStackFramesStart(size_class); const addr = start_ptr + one_trace_size * traces_per_slot * slot_index + @intFromEnum(trace_kind) * @as(usize, one_trace_size); return @ptrCast(@alignCast(addr)); } fn captureStackTrace( bucket: *BucketHeader, ret_addr: usize, size_class: usize, slot_index: SlotIndex, trace_kind: TraceKind, ) void { // Initialize them to 0. When determining the count we must look // for non zero addresses. const stack_addresses = bucket.stackTracePtr(size_class, slot_index, trace_kind); collectStackTrace(ret_addr, stack_addresses); } }; pub fn allocator(self: *Self) Allocator { return .{ .ptr = self, .vtable = &.{ .alloc = alloc, .resize = resize, .free = free, }, }; } fn bucketStackTrace( bucket: *BucketHeader, size_class: usize, slot_index: SlotIndex, trace_kind: TraceKind, ) StackTrace { const stack_addresses = bucket.stackTracePtr(size_class, slot_index, trace_kind); var len: usize = 0; while (len < stack_n and stack_addresses[len] != 0) { len += 1; } return StackTrace{ .instruction_addresses = stack_addresses, .index = len, }; } fn bucketStackFramesStart(size_class: usize) usize { return mem.alignForward( usize, @sizeOf(BucketHeader) + usedBitsCount(size_class), @alignOf(usize), ); } fn bucketSize(size_class: usize) usize { const slot_count = @divExact(page_size, size_class); return bucketStackFramesStart(size_class) + one_trace_size * traces_per_slot * slot_count; } fn usedBitsCount(size_class: usize) usize { const slot_count = @divExact(page_size, size_class); if (slot_count < 8) return 1; return @divExact(slot_count, 8); } fn detectLeaksInBucket( bucket: *BucketHeader, size_class: usize, used_bits_count: usize, ) bool { var leaks = false; var used_bits_byte: usize = 0; while (used_bits_byte < used_bits_count) : (used_bits_byte += 1) { const used_byte = bucket.usedBits(used_bits_byte).*; if (used_byte != 0) { var bit_index: u3 = 0; while (true) : (bit_index += 1) { const is_used = @as(u1, @truncate(used_byte >> bit_index)) != 0; if (is_used) { const slot_index = @as(SlotIndex, @intCast(used_bits_byte * 8 + bit_index)); const stack_trace = bucketStackTrace(bucket, size_class, slot_index, .alloc); const addr = bucket.page + slot_index * size_class; log.err("memory address 0x{x} leaked: {}", .{ @intFromPtr(addr), stack_trace, }); leaks = true; } if (bit_index == math.maxInt(u3)) break; } } } return leaks; } /// Emits log messages for leaks and then returns whether there were any leaks. pub fn detectLeaks(self: *Self) bool { var leaks = false; for (self.buckets, 0..) |optional_bucket, bucket_i| { const first_bucket = optional_bucket orelse continue; const size_class = @as(usize, 1) << @as(math.Log2Int(usize), @intCast(bucket_i)); const used_bits_count = usedBitsCount(size_class); var bucket = first_bucket; while (true) { leaks = detectLeaksInBucket(bucket, size_class, used_bits_count) or leaks; bucket = bucket.next; if (bucket == first_bucket) break; } } var it = self.large_allocations.valueIterator(); while (it.next()) |large_alloc| { if (config.retain_metadata and large_alloc.freed) continue; const stack_trace = large_alloc.getStackTrace(.alloc); log.err("memory address 0x{x} leaked: {}", .{ @intFromPtr(large_alloc.bytes.ptr), stack_trace, }); leaks = true; } return leaks; } fn freeBucket(self: *Self, bucket: *BucketHeader, size_class: usize) void { const bucket_size = bucketSize(size_class); const bucket_slice = @as([*]align(@alignOf(BucketHeader)) u8, @ptrCast(bucket))[0..bucket_size]; self.backing_allocator.free(bucket_slice); } fn freeRetainedMetadata(self: *Self) void { if (config.retain_metadata) { if (config.never_unmap) { // free large allocations that were intentionally leaked by never_unmap var it = self.large_allocations.iterator(); while (it.next()) |large| { if (large.value_ptr.freed) { self.backing_allocator.rawFree(large.value_ptr.bytes, large.value_ptr.log2_ptr_align, @returnAddress()); } } } // free retained metadata for small allocations if (self.empty_buckets) |first_bucket| { var bucket = first_bucket; while (true) { const prev = bucket.prev; if (config.never_unmap) { // free page that was intentionally leaked by never_unmap self.backing_allocator.free(bucket.page[0..page_size]); } // alloc_cursor was set to slot count when bucket added to empty_buckets self.freeBucket(bucket, @divExact(page_size, bucket.alloc_cursor)); bucket = prev; if (bucket == first_bucket) break; } self.empty_buckets = null; } } } pub usingnamespace if (config.retain_metadata) struct { pub fn flushRetainedMetadata(self: *Self) void { self.freeRetainedMetadata(); // also remove entries from large_allocations var it = self.large_allocations.iterator(); while (it.next()) |large| { if (large.value_ptr.freed) { _ = self.large_allocations.remove(@intFromPtr(large.value_ptr.bytes.ptr)); } } } } else struct {}; /// Returns `Check.leak` if there were leaks; `Check.ok` otherwise. pub fn deinit(self: *Self) Check { const leaks = if (config.safety) self.detectLeaks() else false; if (config.retain_metadata) { self.freeRetainedMetadata(); } self.large_allocations.deinit(self.backing_allocator); if (config.safety) { self.small_allocations.deinit(self.backing_allocator); } self.* = undefined; return @as(Check, @enumFromInt(@intFromBool(leaks))); } fn collectStackTrace(first_trace_addr: usize, addresses: *[stack_n]usize) void { if (stack_n == 0) return; @memset(addresses, 0); var stack_trace = StackTrace{ .instruction_addresses = addresses, .index = 0, }; std.debug.captureStackTrace(first_trace_addr, &stack_trace); } fn reportDoubleFree(ret_addr: usize, alloc_stack_trace: StackTrace, free_stack_trace: StackTrace) void { var addresses: [stack_n]usize = [1]usize{0} ** stack_n; var second_free_stack_trace = StackTrace{ .instruction_addresses = &addresses, .index = 0, }; std.debug.captureStackTrace(ret_addr, &second_free_stack_trace); log.err("Double free detected. Allocation: {} First free: {} Second free: {}", .{ alloc_stack_trace, free_stack_trace, second_free_stack_trace, }); } fn allocSlot(self: *Self, size_class: usize, trace_addr: usize) Error![*]u8 { const bucket_index = math.log2(size_class); const first_bucket = self.buckets[bucket_index] orelse try self.createBucket( size_class, bucket_index, ); var bucket = first_bucket; const slot_count = @divExact(page_size, size_class); while (bucket.alloc_cursor == slot_count) { const prev_bucket = bucket; bucket = prev_bucket.next; if (bucket == first_bucket) { // make a new one bucket = try self.createBucket(size_class, bucket_index); bucket.prev = prev_bucket; bucket.next = prev_bucket.next; prev_bucket.next = bucket; bucket.next.prev = bucket; } } // change the allocator's current bucket to be this one self.buckets[bucket_index] = bucket; const slot_index = bucket.alloc_cursor; bucket.alloc_cursor += 1; var used_bits_byte = bucket.usedBits(slot_index / 8); const used_bit_index: u3 = @as(u3, @intCast(slot_index % 8)); // TODO cast should be unnecessary used_bits_byte.* |= (@as(u8, 1) << used_bit_index); bucket.used_count += 1; bucket.captureStackTrace(trace_addr, size_class, slot_index, .alloc); return bucket.page + slot_index * size_class; } fn searchBucket( bucket_list: ?*BucketHeader, addr: usize, ) ?*BucketHeader { const first_bucket = bucket_list orelse return null; var bucket = first_bucket; while (true) { const in_bucket_range = (addr >= @intFromPtr(bucket.page) and addr < @intFromPtr(bucket.page) + page_size); if (in_bucket_range) return bucket; bucket = bucket.prev; if (bucket == first_bucket) { return null; } } } /// This function assumes the object is in the large object storage regardless /// of the parameters. fn resizeLarge( self: *Self, old_mem: []u8, log2_old_align: u8, new_size: usize, ret_addr: usize, ) bool { const entry = self.large_allocations.getEntry(@intFromPtr(old_mem.ptr)) orelse { if (config.safety) { @panic("Invalid free"); } else { unreachable; } }; if (config.retain_metadata and entry.value_ptr.freed) { if (config.safety) { reportDoubleFree(ret_addr, entry.value_ptr.getStackTrace(.alloc), entry.value_ptr.getStackTrace(.free)); @panic("Unrecoverable double free"); } else { unreachable; } } if (config.safety and old_mem.len != entry.value_ptr.bytes.len) { var addresses: [stack_n]usize = [1]usize{0} ** stack_n; var free_stack_trace = StackTrace{ .instruction_addresses = &addresses, .index = 0, }; std.debug.captureStackTrace(ret_addr, &free_stack_trace); log.err("Allocation size {d} bytes does not match free size {d}. Allocation: {} Free: {}", .{ entry.value_ptr.bytes.len, old_mem.len, entry.value_ptr.getStackTrace(.alloc), free_stack_trace, }); } // Do memory limit accounting with requested sizes rather than what // backing_allocator returns because if we want to return // error.OutOfMemory, we have to leave allocation untouched, and // that is impossible to guarantee after calling // backing_allocator.rawResize. const prev_req_bytes = self.total_requested_bytes; if (config.enable_memory_limit) { const new_req_bytes = prev_req_bytes + new_size - entry.value_ptr.requested_size; if (new_req_bytes > prev_req_bytes and new_req_bytes > self.requested_memory_limit) { return false; } self.total_requested_bytes = new_req_bytes; } if (!self.backing_allocator.rawResize(old_mem, log2_old_align, new_size, ret_addr)) { if (config.enable_memory_limit) { self.total_requested_bytes = prev_req_bytes; } return false; } if (config.enable_memory_limit) { entry.value_ptr.requested_size = new_size; } if (config.verbose_log) { log.info("large resize {d} bytes at {*} to {d}", .{ old_mem.len, old_mem.ptr, new_size, }); } entry.value_ptr.bytes = old_mem.ptr[0..new_size]; entry.value_ptr.captureStackTrace(ret_addr, .alloc); return true; } /// This function assumes the object is in the large object storage regardless /// of the parameters. fn freeLarge( self: *Self, old_mem: []u8, log2_old_align: u8, ret_addr: usize, ) void { const entry = self.large_allocations.getEntry(@intFromPtr(old_mem.ptr)) orelse { if (config.safety) { @panic("Invalid free"); } else { unreachable; } }; if (config.retain_metadata and entry.value_ptr.freed) { if (config.safety) { reportDoubleFree(ret_addr, entry.value_ptr.getStackTrace(.alloc), entry.value_ptr.getStackTrace(.free)); return; } else { unreachable; } } if (config.safety and old_mem.len != entry.value_ptr.bytes.len) { var addresses: [stack_n]usize = [1]usize{0} ** stack_n; var free_stack_trace = StackTrace{ .instruction_addresses = &addresses, .index = 0, }; std.debug.captureStackTrace(ret_addr, &free_stack_trace); log.err("Allocation size {d} bytes does not match free size {d}. Allocation: {} Free: {}", .{ entry.value_ptr.bytes.len, old_mem.len, entry.value_ptr.getStackTrace(.alloc), free_stack_trace, }); } if (!config.never_unmap) { self.backing_allocator.rawFree(old_mem, log2_old_align, ret_addr); } if (config.enable_memory_limit) { self.total_requested_bytes -= entry.value_ptr.requested_size; } if (config.verbose_log) { log.info("large free {d} bytes at {*}", .{ old_mem.len, old_mem.ptr }); } if (!config.retain_metadata) { assert(self.large_allocations.remove(@intFromPtr(old_mem.ptr))); } else { entry.value_ptr.freed = true; entry.value_ptr.captureStackTrace(ret_addr, .free); } } pub fn setRequestedMemoryLimit(self: *Self, limit: usize) void { self.requested_memory_limit = limit; } fn resize( ctx: *anyopaque, old_mem: []u8, log2_old_align_u8: u8, new_size: usize, ret_addr: usize, ) bool { const self: *Self = @ptrCast(@alignCast(ctx)); const log2_old_align = @as(Allocator.Log2Align, @intCast(log2_old_align_u8)); self.mutex.lock(); defer self.mutex.unlock(); assert(old_mem.len != 0); const aligned_size = @max(old_mem.len, @as(usize, 1) << log2_old_align); if (aligned_size > largest_bucket_object_size) { return self.resizeLarge(old_mem, log2_old_align, new_size, ret_addr); } const size_class_hint = math.ceilPowerOfTwoAssert(usize, aligned_size); var bucket_index = math.log2(size_class_hint); var size_class: usize = size_class_hint; const bucket = while (bucket_index < small_bucket_count) : (bucket_index += 1) { if (searchBucket(self.buckets[bucket_index], @intFromPtr(old_mem.ptr))) |bucket| { // move bucket to head of list to optimize search for nearby allocations self.buckets[bucket_index] = bucket; break bucket; } size_class *= 2; } else blk: { if (config.retain_metadata) { if (!self.large_allocations.contains(@intFromPtr(old_mem.ptr))) { // object not in active buckets or a large allocation, so search empty buckets if (searchBucket(self.empty_buckets, @intFromPtr(old_mem.ptr))) |bucket| { // bucket is empty so is_used below will always be false and we exit there break :blk bucket; } else { @panic("Invalid free"); } } } return self.resizeLarge(old_mem, log2_old_align, new_size, ret_addr); }; const byte_offset = @intFromPtr(old_mem.ptr) - @intFromPtr(bucket.page); const slot_index = @as(SlotIndex, @intCast(byte_offset / size_class)); const used_byte_index = slot_index / 8; const used_bit_index = @as(u3, @intCast(slot_index % 8)); const used_byte = bucket.usedBits(used_byte_index); const is_used = @as(u1, @truncate(used_byte.* >> used_bit_index)) != 0; if (!is_used) { if (config.safety) { reportDoubleFree(ret_addr, bucketStackTrace(bucket, size_class, slot_index, .alloc), bucketStackTrace(bucket, size_class, slot_index, .free)); @panic("Unrecoverable double free"); } else { unreachable; } } // Definitely an in-use small alloc now. if (config.safety) { const entry = self.small_allocations.getEntry(@intFromPtr(old_mem.ptr)) orelse @panic("Invalid free"); if (old_mem.len != entry.value_ptr.requested_size or log2_old_align != entry.value_ptr.log2_ptr_align) { var addresses: [stack_n]usize = [1]usize{0} ** stack_n; var free_stack_trace = StackTrace{ .instruction_addresses = &addresses, .index = 0, }; std.debug.captureStackTrace(ret_addr, &free_stack_trace); if (old_mem.len != entry.value_ptr.requested_size) { log.err("Allocation size {d} bytes does not match resize size {d}. Allocation: {} Resize: {}", .{ entry.value_ptr.requested_size, old_mem.len, bucketStackTrace(bucket, size_class, slot_index, .alloc), free_stack_trace, }); } if (log2_old_align != entry.value_ptr.log2_ptr_align) { log.err("Allocation alignment {d} does not match resize alignment {d}. Allocation: {} Resize: {}", .{ @as(usize, 1) << @as(math.Log2Int(usize), @intCast(entry.value_ptr.log2_ptr_align)), @as(usize, 1) << @as(math.Log2Int(usize), @intCast(log2_old_align)), bucketStackTrace(bucket, size_class, slot_index, .alloc), free_stack_trace, }); } } } const prev_req_bytes = self.total_requested_bytes; if (config.enable_memory_limit) { const new_req_bytes = prev_req_bytes + new_size - old_mem.len; if (new_req_bytes > prev_req_bytes and new_req_bytes > self.requested_memory_limit) { return false; } self.total_requested_bytes = new_req_bytes; } const new_aligned_size = @max(new_size, @as(usize, 1) << log2_old_align); const new_size_class = math.ceilPowerOfTwoAssert(usize, new_aligned_size); if (new_size_class <= size_class) { if (old_mem.len > new_size) { @memset(old_mem[new_size..], undefined); } if (config.verbose_log) { log.info("small resize {d} bytes at {*} to {d}", .{ old_mem.len, old_mem.ptr, new_size, }); } if (config.safety) { const entry = self.small_allocations.getEntry(@intFromPtr(old_mem.ptr)).?; entry.value_ptr.requested_size = new_size; } return true; } if (config.enable_memory_limit) { self.total_requested_bytes = prev_req_bytes; } return false; } fn free( ctx: *anyopaque, old_mem: []u8, log2_old_align_u8: u8, ret_addr: usize, ) void { const self: *Self = @ptrCast(@alignCast(ctx)); const log2_old_align = @as(Allocator.Log2Align, @intCast(log2_old_align_u8)); self.mutex.lock(); defer self.mutex.unlock(); assert(old_mem.len != 0); const aligned_size = @max(old_mem.len, @as(usize, 1) << log2_old_align); if (aligned_size > largest_bucket_object_size) { self.freeLarge(old_mem, log2_old_align, ret_addr); return; } const size_class_hint = math.ceilPowerOfTwoAssert(usize, aligned_size); var bucket_index = math.log2(size_class_hint); var size_class: usize = size_class_hint; const bucket = while (bucket_index < small_bucket_count) : (bucket_index += 1) { if (searchBucket(self.buckets[bucket_index], @intFromPtr(old_mem.ptr))) |bucket| { // move bucket to head of list to optimize search for nearby allocations self.buckets[bucket_index] = bucket; break bucket; } size_class *= 2; } else blk: { if (config.retain_metadata) { if (!self.large_allocations.contains(@intFromPtr(old_mem.ptr))) { // object not in active buckets or a large allocation, so search empty buckets if (searchBucket(self.empty_buckets, @intFromPtr(old_mem.ptr))) |bucket| { // bucket is empty so is_used below will always be false and we exit there break :blk bucket; } else { @panic("Invalid free"); } } } self.freeLarge(old_mem, log2_old_align, ret_addr); return; }; const byte_offset = @intFromPtr(old_mem.ptr) - @intFromPtr(bucket.page); const slot_index = @as(SlotIndex, @intCast(byte_offset / size_class)); const used_byte_index = slot_index / 8; const used_bit_index = @as(u3, @intCast(slot_index % 8)); const used_byte = bucket.usedBits(used_byte_index); const is_used = @as(u1, @truncate(used_byte.* >> used_bit_index)) != 0; if (!is_used) { if (config.safety) { reportDoubleFree(ret_addr, bucketStackTrace(bucket, size_class, slot_index, .alloc), bucketStackTrace(bucket, size_class, slot_index, .free)); // Recoverable if this is a free. return; } else { unreachable; } } // Definitely an in-use small alloc now. if (config.safety) { const entry = self.small_allocations.getEntry(@intFromPtr(old_mem.ptr)) orelse @panic("Invalid free"); if (old_mem.len != entry.value_ptr.requested_size or log2_old_align != entry.value_ptr.log2_ptr_align) { var addresses: [stack_n]usize = [1]usize{0} ** stack_n; var free_stack_trace = StackTrace{ .instruction_addresses = &addresses, .index = 0, }; std.debug.captureStackTrace(ret_addr, &free_stack_trace); if (old_mem.len != entry.value_ptr.requested_size) { log.err("Allocation size {d} bytes does not match free size {d}. Allocation: {} Free: {}", .{ entry.value_ptr.requested_size, old_mem.len, bucketStackTrace(bucket, size_class, slot_index, .alloc), free_stack_trace, }); } if (log2_old_align != entry.value_ptr.log2_ptr_align) { log.err("Allocation alignment {d} does not match free alignment {d}. Allocation: {} Free: {}", .{ @as(usize, 1) << @as(math.Log2Int(usize), @intCast(entry.value_ptr.log2_ptr_align)), @as(usize, 1) << @as(math.Log2Int(usize), @intCast(log2_old_align)), bucketStackTrace(bucket, size_class, slot_index, .alloc), free_stack_trace, }); } } } if (config.enable_memory_limit) { self.total_requested_bytes -= old_mem.len; } // Capture stack trace to be the "first free", in case a double free happens. bucket.captureStackTrace(ret_addr, size_class, slot_index, .free); used_byte.* &= ~(@as(u8, 1) << used_bit_index); bucket.used_count -= 1; if (bucket.used_count == 0) { if (bucket.next == bucket) { // it's the only bucket and therefore the current one self.buckets[bucket_index] = null; } else { bucket.next.prev = bucket.prev; bucket.prev.next = bucket.next; self.buckets[bucket_index] = bucket.prev; } if (!config.never_unmap) { self.backing_allocator.free(bucket.page[0..page_size]); } if (!config.retain_metadata) { self.freeBucket(bucket, size_class); } else { // move alloc_cursor to end so we can tell size_class later const slot_count = @divExact(page_size, size_class); bucket.alloc_cursor = @as(SlotIndex, @truncate(slot_count)); if (self.empty_buckets) |prev_bucket| { // empty_buckets is ordered newest to oldest through prev so that if // config.never_unmap is false and backing_allocator reuses freed memory // then searchBuckets will always return the newer, relevant bucket bucket.prev = prev_bucket; bucket.next = prev_bucket.next; prev_bucket.next = bucket; bucket.next.prev = bucket; } else { bucket.prev = bucket; bucket.next = bucket; } self.empty_buckets = bucket; } } else { @memset(old_mem, undefined); } if (config.safety) { assert(self.small_allocations.remove(@intFromPtr(old_mem.ptr))); } if (config.verbose_log) { log.info("small free {d} bytes at {*}", .{ old_mem.len, old_mem.ptr }); } } // Returns true if an allocation of `size` bytes is within the specified // limits if enable_memory_limit is true fn isAllocationAllowed(self: *Self, size: usize) bool { if (config.enable_memory_limit) { const new_req_bytes = self.total_requested_bytes + size; if (new_req_bytes > self.requested_memory_limit) return false; self.total_requested_bytes = new_req_bytes; } return true; } fn alloc(ctx: *anyopaque, len: usize, log2_ptr_align: u8, ret_addr: usize) ?[*]u8 { const self: *Self = @ptrCast(@alignCast(ctx)); self.mutex.lock(); defer self.mutex.unlock(); if (!self.isAllocationAllowed(len)) return null; return allocInner(self, len, @as(Allocator.Log2Align, @intCast(log2_ptr_align)), ret_addr) catch return null; } fn allocInner( self: *Self, len: usize, log2_ptr_align: Allocator.Log2Align, ret_addr: usize, ) Allocator.Error![*]u8 { const new_aligned_size = @max(len, @as(usize, 1) << @as(Allocator.Log2Align, @intCast(log2_ptr_align))); if (new_aligned_size > largest_bucket_object_size) { try self.large_allocations.ensureUnusedCapacity(self.backing_allocator, 1); const ptr = self.backing_allocator.rawAlloc(len, log2_ptr_align, ret_addr) orelse return error.OutOfMemory; const slice = ptr[0..len]; const gop = self.large_allocations.getOrPutAssumeCapacity(@intFromPtr(slice.ptr)); if (config.retain_metadata and !config.never_unmap) { // Backing allocator may be reusing memory that we're retaining metadata for assert(!gop.found_existing or gop.value_ptr.freed); } else { assert(!gop.found_existing); // This would mean the kernel double-mapped pages. } gop.value_ptr.bytes = slice; if (config.enable_memory_limit) gop.value_ptr.requested_size = len; gop.value_ptr.captureStackTrace(ret_addr, .alloc); if (config.retain_metadata) { gop.value_ptr.freed = false; if (config.never_unmap) { gop.value_ptr.log2_ptr_align = log2_ptr_align; } } if (config.verbose_log) { log.info("large alloc {d} bytes at {*}", .{ slice.len, slice.ptr }); } return slice.ptr; } if (config.safety) { try self.small_allocations.ensureUnusedCapacity(self.backing_allocator, 1); } const new_size_class = math.ceilPowerOfTwoAssert(usize, new_aligned_size); const ptr = try self.allocSlot(new_size_class, ret_addr); if (config.safety) { const gop = self.small_allocations.getOrPutAssumeCapacity(@intFromPtr(ptr)); gop.value_ptr.requested_size = len; gop.value_ptr.log2_ptr_align = log2_ptr_align; } if (config.verbose_log) { log.info("small alloc {d} bytes at {*}", .{ len, ptr }); } return ptr; } fn createBucket(self: *Self, size_class: usize, bucket_index: usize) Error!*BucketHeader { const page = try self.backing_allocator.alignedAlloc(u8, page_size, page_size); errdefer self.backing_allocator.free(page); const bucket_size = bucketSize(size_class); const bucket_bytes = try self.backing_allocator.alignedAlloc(u8, @alignOf(BucketHeader), bucket_size); const ptr = @as(*BucketHeader, @ptrCast(bucket_bytes.ptr)); ptr.* = BucketHeader{ .prev = ptr, .next = ptr, .page = page.ptr, .alloc_cursor = 0, .used_count = 0, }; self.buckets[bucket_index] = ptr; // Set the used bits to all zeroes @memset(@as([*]u8, @as(*[1]u8, ptr.usedBits(0)))[0..usedBitsCount(size_class)], 0); return ptr; } }; } const TraceKind = enum { alloc, free, }; const test_config = Config{}; test "small allocations - free in same order" { var gpa = GeneralPurposeAllocator(test_config){}; defer std.testing.expect(gpa.deinit() == .ok) catch @panic("leak"); const allocator = gpa.allocator(); var list = std.ArrayList(*u64).init(std.testing.allocator); defer list.deinit(); var i: usize = 0; while (i < 513) : (i += 1) { const ptr = try allocator.create(u64); try list.append(ptr); } for (list.items) |ptr| { allocator.destroy(ptr); } } test "small allocations - free in reverse order" { var gpa = GeneralPurposeAllocator(test_config){}; defer std.testing.expect(gpa.deinit() == .ok) catch @panic("leak"); const allocator = gpa.allocator(); var list = std.ArrayList(*u64).init(std.testing.allocator); defer list.deinit(); var i: usize = 0; while (i < 513) : (i += 1) { const ptr = try allocator.create(u64); try list.append(ptr); } while (list.popOrNull()) |ptr| { allocator.destroy(ptr); } } test "large allocations" { var gpa = GeneralPurposeAllocator(test_config){}; defer std.testing.expect(gpa.deinit() == .ok) catch @panic("leak"); const allocator = gpa.allocator(); const ptr1 = try allocator.alloc(u64, 42768); const ptr2 = try allocator.alloc(u64, 52768); allocator.free(ptr1); const ptr3 = try allocator.alloc(u64, 62768); allocator.free(ptr3); allocator.free(ptr2); } test "very large allocation" { var gpa = GeneralPurposeAllocator(test_config){}; defer std.testing.expect(gpa.deinit() == .ok) catch @panic("leak"); const allocator = gpa.allocator(); try std.testing.expectError(error.OutOfMemory, allocator.alloc(u8, math.maxInt(usize))); } test "realloc" { var gpa = GeneralPurposeAllocator(test_config){}; defer std.testing.expect(gpa.deinit() == .ok) catch @panic("leak"); const allocator = gpa.allocator(); var slice = try allocator.alignedAlloc(u8, @alignOf(u32), 1); defer allocator.free(slice); slice[0] = 0x12; // This reallocation should keep its pointer address. const old_slice = slice; slice = try allocator.realloc(slice, 2); try std.testing.expect(old_slice.ptr == slice.ptr); try std.testing.expect(slice[0] == 0x12); slice[1] = 0x34; // This requires upgrading to a larger size class slice = try allocator.realloc(slice, 17); try std.testing.expect(slice[0] == 0x12); try std.testing.expect(slice[1] == 0x34); } test "shrink" { var gpa = GeneralPurposeAllocator(test_config){}; defer std.testing.expect(gpa.deinit() == .ok) catch @panic("leak"); const allocator = gpa.allocator(); var slice = try allocator.alloc(u8, 20); defer allocator.free(slice); @memset(slice, 0x11); try std.testing.expect(allocator.resize(slice, 17)); slice = slice[0..17]; for (slice) |b| { try std.testing.expect(b == 0x11); } try std.testing.expect(allocator.resize(slice, 16)); slice = slice[0..16]; for (slice) |b| { try std.testing.expect(b == 0x11); } } test "large object - grow" { var gpa = GeneralPurposeAllocator(test_config){}; defer std.testing.expect(gpa.deinit() == .ok) catch @panic("leak"); const allocator = gpa.allocator(); var slice1 = try allocator.alloc(u8, page_size * 2 - 20); defer allocator.free(slice1); const old = slice1; slice1 = try allocator.realloc(slice1, page_size * 2 - 10); try std.testing.expect(slice1.ptr == old.ptr); slice1 = try allocator.realloc(slice1, page_size * 2); try std.testing.expect(slice1.ptr == old.ptr); slice1 = try allocator.realloc(slice1, page_size * 2 + 1); } test "realloc small object to large object" { var gpa = GeneralPurposeAllocator(test_config){}; defer std.testing.expect(gpa.deinit() == .ok) catch @panic("leak"); const allocator = gpa.allocator(); var slice = try allocator.alloc(u8, 70); defer allocator.free(slice); slice[0] = 0x12; slice[60] = 0x34; // This requires upgrading to a large object const large_object_size = page_size * 2 + 50; slice = try allocator.realloc(slice, large_object_size); try std.testing.expect(slice[0] == 0x12); try std.testing.expect(slice[60] == 0x34); } test "shrink large object to large object" { var gpa = GeneralPurposeAllocator(test_config){}; defer std.testing.expect(gpa.deinit() == .ok) catch @panic("leak"); const allocator = gpa.allocator(); var slice = try allocator.alloc(u8, page_size * 2 + 50); defer allocator.free(slice); slice[0] = 0x12; slice[60] = 0x34; if (!allocator.resize(slice, page_size * 2 + 1)) return; slice = slice.ptr[0 .. page_size * 2 + 1]; try std.testing.expect(slice[0] == 0x12); try std.testing.expect(slice[60] == 0x34); try std.testing.expect(allocator.resize(slice, page_size * 2 + 1)); slice = slice[0 .. page_size * 2 + 1]; try std.testing.expect(slice[0] == 0x12); try std.testing.expect(slice[60] == 0x34); slice = try allocator.realloc(slice, page_size * 2); try std.testing.expect(slice[0] == 0x12); try std.testing.expect(slice[60] == 0x34); } test "shrink large object to large object with larger alignment" { var gpa = GeneralPurposeAllocator(test_config){}; defer std.testing.expect(gpa.deinit() == .ok) catch @panic("leak"); const allocator = gpa.allocator(); var debug_buffer: [1000]u8 = undefined; var fba = std.heap.FixedBufferAllocator.init(&debug_buffer); const debug_allocator = fba.allocator(); const alloc_size = page_size * 2 + 50; var slice = try allocator.alignedAlloc(u8, 16, alloc_size); defer allocator.free(slice); const big_alignment: usize = switch (builtin.os.tag) { .windows => page_size * 32, // Windows aligns to 64K. else => page_size * 2, }; // This loop allocates until we find a page that is not aligned to the big // alignment. Then we shrink the allocation after the loop, but increase the // alignment to the higher one, that we know will force it to realloc. var stuff_to_free = std.ArrayList([]align(16) u8).init(debug_allocator); while (mem.isAligned(@intFromPtr(slice.ptr), big_alignment)) { try stuff_to_free.append(slice); slice = try allocator.alignedAlloc(u8, 16, alloc_size); } while (stuff_to_free.popOrNull()) |item| { allocator.free(item); } slice[0] = 0x12; slice[60] = 0x34; slice = try allocator.reallocAdvanced(slice, big_alignment, alloc_size / 2); try std.testing.expect(slice[0] == 0x12); try std.testing.expect(slice[60] == 0x34); } test "realloc large object to small object" { var gpa = GeneralPurposeAllocator(test_config){}; defer std.testing.expect(gpa.deinit() == .ok) catch @panic("leak"); const allocator = gpa.allocator(); var slice = try allocator.alloc(u8, page_size * 2 + 50); defer allocator.free(slice); slice[0] = 0x12; slice[16] = 0x34; slice = try allocator.realloc(slice, 19); try std.testing.expect(slice[0] == 0x12); try std.testing.expect(slice[16] == 0x34); } test "overridable mutexes" { var gpa = GeneralPurposeAllocator(.{ .MutexType = std.Thread.Mutex }){ .backing_allocator = std.testing.allocator, .mutex = std.Thread.Mutex{}, }; defer std.testing.expect(gpa.deinit() == .ok) catch @panic("leak"); const allocator = gpa.allocator(); const ptr = try allocator.create(i32); defer allocator.destroy(ptr); } test "non-page-allocator backing allocator" { var gpa = GeneralPurposeAllocator(.{}){ .backing_allocator = std.testing.allocator }; defer std.testing.expect(gpa.deinit() == .ok) catch @panic("leak"); const allocator = gpa.allocator(); const ptr = try allocator.create(i32); defer allocator.destroy(ptr); } test "realloc large object to larger alignment" { var gpa = GeneralPurposeAllocator(test_config){}; defer std.testing.expect(gpa.deinit() == .ok) catch @panic("leak"); const allocator = gpa.allocator(); var debug_buffer: [1000]u8 = undefined; var fba = std.heap.FixedBufferAllocator.init(&debug_buffer); const debug_allocator = fba.allocator(); var slice = try allocator.alignedAlloc(u8, 16, page_size * 2 + 50); defer allocator.free(slice); const big_alignment: usize = switch (builtin.os.tag) { .windows => page_size * 32, // Windows aligns to 64K. else => page_size * 2, }; // This loop allocates until we find a page that is not aligned to the big alignment. var stuff_to_free = std.ArrayList([]align(16) u8).init(debug_allocator); while (mem.isAligned(@intFromPtr(slice.ptr), big_alignment)) { try stuff_to_free.append(slice); slice = try allocator.alignedAlloc(u8, 16, page_size * 2 + 50); } while (stuff_to_free.popOrNull()) |item| { allocator.free(item); } slice[0] = 0x12; slice[16] = 0x34; slice = try allocator.reallocAdvanced(slice, 32, page_size * 2 + 100); try std.testing.expect(slice[0] == 0x12); try std.testing.expect(slice[16] == 0x34); slice = try allocator.reallocAdvanced(slice, 32, page_size * 2 + 25); try std.testing.expect(slice[0] == 0x12); try std.testing.expect(slice[16] == 0x34); slice = try allocator.reallocAdvanced(slice, big_alignment, page_size * 2 + 100); try std.testing.expect(slice[0] == 0x12); try std.testing.expect(slice[16] == 0x34); } test "large object shrinks to small but allocation fails during shrink" { var failing_allocator = std.testing.FailingAllocator.init(std.heap.page_allocator, 3); var gpa = GeneralPurposeAllocator(.{}){ .backing_allocator = failing_allocator.allocator() }; defer std.testing.expect(gpa.deinit() == .ok) catch @panic("leak"); const allocator = gpa.allocator(); var slice = try allocator.alloc(u8, page_size * 2 + 50); defer allocator.free(slice); slice[0] = 0x12; slice[3] = 0x34; // Next allocation will fail in the backing allocator of the GeneralPurposeAllocator try std.testing.expect(allocator.resize(slice, 4)); slice = slice[0..4]; try std.testing.expect(slice[0] == 0x12); try std.testing.expect(slice[3] == 0x34); } test "objects of size 1024 and 2048" { var gpa = GeneralPurposeAllocator(test_config){}; defer std.testing.expect(gpa.deinit() == .ok) catch @panic("leak"); const allocator = gpa.allocator(); const slice = try allocator.alloc(u8, 1025); const slice2 = try allocator.alloc(u8, 3000); allocator.free(slice); allocator.free(slice2); } test "setting a memory cap" { var gpa = GeneralPurposeAllocator(.{ .enable_memory_limit = true }){}; defer std.testing.expect(gpa.deinit() == .ok) catch @panic("leak"); const allocator = gpa.allocator(); gpa.setRequestedMemoryLimit(1010); const small = try allocator.create(i32); try std.testing.expect(gpa.total_requested_bytes == 4); const big = try allocator.alloc(u8, 1000); try std.testing.expect(gpa.total_requested_bytes == 1004); try std.testing.expectError(error.OutOfMemory, allocator.create(u64)); allocator.destroy(small); try std.testing.expect(gpa.total_requested_bytes == 1000); allocator.free(big); try std.testing.expect(gpa.total_requested_bytes == 0); const exact = try allocator.alloc(u8, 1010); try std.testing.expect(gpa.total_requested_bytes == 1010); allocator.free(exact); } test "double frees" { // use a GPA to back a GPA to check for leaks of the latter's metadata var backing_gpa = GeneralPurposeAllocator(.{ .safety = true }){}; defer std.testing.expect(backing_gpa.deinit() == .ok) catch @panic("leak"); const GPA = GeneralPurposeAllocator(.{ .safety = true, .never_unmap = true, .retain_metadata = true }); var gpa = GPA{ .backing_allocator = backing_gpa.allocator() }; defer std.testing.expect(gpa.deinit() == .ok) catch @panic("leak"); const allocator = gpa.allocator(); // detect a small allocation double free, even though bucket is emptied const index: usize = 6; const size_class: usize = @as(usize, 1) << 6; const small = try allocator.alloc(u8, size_class); try std.testing.expect(GPA.searchBucket(gpa.buckets[index], @intFromPtr(small.ptr)) != null); allocator.free(small); try std.testing.expect(GPA.searchBucket(gpa.buckets[index], @intFromPtr(small.ptr)) == null); try std.testing.expect(GPA.searchBucket(gpa.empty_buckets, @intFromPtr(small.ptr)) != null); // detect a large allocation double free const large = try allocator.alloc(u8, 2 * page_size); try std.testing.expect(gpa.large_allocations.contains(@intFromPtr(large.ptr))); try std.testing.expectEqual(gpa.large_allocations.getEntry(@intFromPtr(large.ptr)).?.value_ptr.bytes, large); allocator.free(large); try std.testing.expect(gpa.large_allocations.contains(@intFromPtr(large.ptr))); try std.testing.expect(gpa.large_allocations.getEntry(@intFromPtr(large.ptr)).?.value_ptr.freed); const normal_small = try allocator.alloc(u8, size_class); defer allocator.free(normal_small); const normal_large = try allocator.alloc(u8, 2 * page_size); defer allocator.free(normal_large); // check that flushing retained metadata doesn't disturb live allocations gpa.flushRetainedMetadata(); try std.testing.expect(gpa.empty_buckets == null); try std.testing.expect(GPA.searchBucket(gpa.buckets[index], @intFromPtr(normal_small.ptr)) != null); try std.testing.expect(gpa.large_allocations.contains(@intFromPtr(normal_large.ptr))); try std.testing.expect(!gpa.large_allocations.contains(@intFromPtr(large.ptr))); } test "bug 9995 fix, large allocs count requested size not backing size" { // with AtLeast, buffer likely to be larger than requested, especially when shrinking var gpa = GeneralPurposeAllocator(.{ .enable_memory_limit = true }){}; const allocator = gpa.allocator(); var buf = try allocator.alignedAlloc(u8, 1, page_size + 1); try std.testing.expect(gpa.total_requested_bytes == page_size + 1); buf = try allocator.realloc(buf, 1); try std.testing.expect(gpa.total_requested_bytes == 1); buf = try allocator.realloc(buf, 2); try std.testing.expect(gpa.total_requested_bytes == 2); }