1- //! # General Purpose Allocator
1+ //! An allocator that is intended to be used in Debug mode.
22//!
3- //! ## Design Priorities
3+ //! ## Features
44//!
5- //! ### `OptimizationMode.debug` and `OptimizationMode.release_safe`:
5+ //! * Captures stack traces on allocation, free, and optionally resize.
6+ //! * Double free detection, which prints all three traces (first alloc, first
7+ //! free, second free).
8+ //! * Leak detection, with stack traces.
9+ //! * Never reuses memory addresses, making it easier for Zig to detect branch
10+ //! on undefined values in case of dangling pointers. This relies on
11+ //! the backing allocator to also not reuse addresses.
12+ //! * Uses a minimum backing allocation size to avoid operating system errors
13+ //! from having too many active memory mappings.
14+ //! * When a page of memory is no longer needed, give it back to resident
15+ //! memory as soon as possible, so that it causes page faults when used.
16+ //! * Cross platform. Operates based on a backing allocator which makes it work
17+ //! everywhere, even freestanding.
18+ //! * Compile-time configuration.
619//!
7- //! * Detect double free, and emit stack trace of:
8- //! - Where it was first allocated
9- //! - Where it was freed the first time
10- //! - Where it was freed the second time
20+ //! These features require the allocator to be quite slow and wasteful. For
21+ //! example, when allocating a single byte, the efficiency is less than 1%;
22+ //! it requires more than 100 bytes of overhead to manage the allocation for
23+ //! one byte. The efficiency gets better with larger allocations.
1124//!
12- //! * Detect leaks and emit stack trace of:
13- //! - Where it was allocated
25+ //! ## Basic Design
1426//!
15- //! * When a page of memory is no longer needed, give it back to resident memory
16- //! as soon as possible, so that it causes page faults when used.
27+ //! Allocations are divided into two categories, small and large.
1728//!
18- //! * Do not re-use memory slots, so that memory safety is upheld. For small
19- //! allocations, this is handled here; for larger ones it is handled in the
20- //! backing allocator (by default `std.heap.page_allocator`).
21- //!
22- //! * Make pointer math errors unlikely to harm memory from
23- //! unrelated allocations.
24- //!
25- //! * It's OK for these mechanisms to cost some extra overhead bytes.
26- //!
27- //! * It's OK for performance cost for these mechanisms.
28- //!
29- //! * Rogue memory writes should not harm the allocator's state.
30- //!
31- //! * Cross platform. Operates based on a backing allocator which makes it work
32- //! everywhere, even freestanding.
33- //!
34- //! * Compile-time configuration.
35- //!
36- //! ### `OptimizationMode.release_fast` (note: not much work has gone into this use case yet):
37- //!
38- //! * Low fragmentation is primary concern
39- //! * Performance of worst-case latency is secondary concern
40- //! * Performance of average-case latency is next
41- //! * Finally, having freed memory unmapped, and pointer math errors unlikely to
42- //! harm memory from unrelated allocations are nice-to-haves.
43- //!
44- //! ### `OptimizationMode.release_small` (note: not much work has gone into this use case yet):
45- //!
46- //! * Small binary code size of the executable is the primary concern.
47- //! * Next, defer to the `.release_fast` priority list.
48- //!
49- //! ## Basic Design:
50- //!
51- //! Small allocations are divided into buckets:
29+ //! Small allocations are divided into buckets based on `page_size`:
5230//!
5331//! ```
5432//! index obj_size
6442//! 9 512
6543//! 10 1024
6644//! 11 2048
45+ //! ...
6746//! ```
6847//!
48+ //! This goes on for `small_bucket_count` indexes.
49+ //!
50+ //! Allocations are grouped into an object size based on max(len, alignment),
51+ //! rounded up to the next power of two.
52+ //!
6953//! The main allocator state has an array of all the "current" buckets for each
7054//! size class. Each slot in the array can be null, meaning the bucket for that
7155//! size class is not allocated. When the first object is allocated for a given
72- //! size class, it allocates 1 page of memory from the OS. This page is
73- //! divided into "slots" - one per allocated object. Along with the page of memory
74- //! for object slots, as many pages as necessary are allocated to store the
75- //! BucketHeader, followed by "used bits", and two stack traces for each slot
76- //! (allocation trace and free trace).
56+ //! size class, it makes one `page_size` allocation from the backing allocator.
57+ //! This allocation is divided into "slots" - one per allocated object, leaving
58+ //! room for the allocation metadata (starting with `BucketHeader`), which is
59+ //! located at the very end of the "page".
60+ //!
61+ //! The allocation metadata includes "used bits" - 1 bit per slot representing
62+ //! whether the slot is used. Allocations always take the next available slot
63+ //! from the current bucket, setting the corresponding used bit, as well as
64+ //! incrementing `allocated_count`.
7765//!
78- //! The "used bits" are 1 bit per slot representing whether the slot is used.
79- //! Allocations use the data to iterate to find a free slot. Frees assert that the
80- //! corresponding bit is 1 and set it to 0.
66+ //! Frees recover the allocation metadata based on the address, length, and
67+ //! alignment, relying on the backing allocation's large alignment, combined
68+ //! with the fact that allocations are never moved from small to large, or vice
69+ //! versa.
8170//!
82- //! Buckets have prev and next pointers. When there is only one bucket for a given
83- //! size class, both prev and next point to itself. When all slots of a bucket are
84- //! used, a new bucket is allocated, and enters the doubly linked list. The main
85- //! allocator state tracks the "current" bucket for each size class. Leak detection
86- //! currently only checks the current bucket.
71+ //! When a bucket is full, a new one is allocated, containing a pointer to the
72+ //! previous one. This singly-linked list is iterated during leak detection.
8773//!
88- //! Resizing detects if the size class is unchanged or smaller, in which case the same
89- //! pointer is returned unmodified. If a larger size class is required,
90- //! `error.OutOfMemory` is returned .
74+ //! Resizing and remapping work the same on small allocations: if the size
75+ //! class would not change, then the operation succeeds, and the address is
76+ //! unchanged. Otherwise, the request is rejected .
9177//!
92- //! Large objects are allocated directly using the backing allocator and their metadata is stored
93- //! in a `std.HashMap` using the backing allocator.
78+ //! Large objects are allocated directly using the backing allocator. Metadata
79+ //! is stored separately in a `std.HashMap` using the backing allocator.
80+ //!
81+ //! Resizing and remapping are forwarded directly to the backing allocator,
82+ //! except where such operations would change the category from large to small.
9483
9584const std = @import ("std" );
9685const builtin = @import ("builtin" );
@@ -172,10 +161,8 @@ pub const Config = struct {
172161 canary : usize = @truncate (0x9232a6ff85dff10f ),
173162};
174163
175- pub const Check = enum { ok , leak };
176-
177164/// Default initialization of this struct is deprecated; use `.init` instead.
178- pub fn GeneralPurposeAllocator (comptime config : Config ) type {
165+ pub fn DebugAllocator (comptime config : Config ) type {
179166 return struct {
180167 backing_allocator : Allocator = std .heap .page_allocator ,
181168 /// Tracks the active bucket, which is the one that has free slots in it.
@@ -491,8 +478,8 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {
491478 }
492479 }
493480
494- /// Returns `Check.leak` if there were leaks; `Check.ok` otherwise.
495- pub fn deinit (self : * Self ) Check {
481+ /// Returns `std.heap. Check.leak` if there were leaks; `std.heap. Check.ok` otherwise.
482+ pub fn deinit (self : * Self ) std.heap. Check {
496483 const leaks = if (config .safety ) self .detectLeaks () else false ;
497484 if (config .retain_metadata ) self .freeRetainedMetadata ();
498485 self .large_allocations .deinit (self .backing_allocator );
@@ -1041,7 +1028,7 @@ const TraceKind = enum {
10411028const test_config = Config {};
10421029
10431030test "small allocations - free in same order" {
1044- var gpa = GeneralPurposeAllocator (test_config ){};
1031+ var gpa = DebugAllocator (test_config ){};
10451032 defer std .testing .expect (gpa .deinit () == .ok ) catch @panic ("leak" );
10461033 const allocator = gpa .allocator ();
10471034
@@ -1060,7 +1047,7 @@ test "small allocations - free in same order" {
10601047}
10611048
10621049test "small allocations - free in reverse order" {
1063- var gpa = GeneralPurposeAllocator (test_config ){};
1050+ var gpa = DebugAllocator (test_config ){};
10641051 defer std .testing .expect (gpa .deinit () == .ok ) catch @panic ("leak" );
10651052 const allocator = gpa .allocator ();
10661053
@@ -1079,7 +1066,7 @@ test "small allocations - free in reverse order" {
10791066}
10801067
10811068test "large allocations" {
1082- var gpa = GeneralPurposeAllocator (test_config ){};
1069+ var gpa = DebugAllocator (test_config ){};
10831070 defer std .testing .expect (gpa .deinit () == .ok ) catch @panic ("leak" );
10841071 const allocator = gpa .allocator ();
10851072
@@ -1092,15 +1079,15 @@ test "large allocations" {
10921079}
10931080
10941081test "very large allocation" {
1095- var gpa = GeneralPurposeAllocator (test_config ){};
1082+ var gpa = DebugAllocator (test_config ){};
10961083 defer std .testing .expect (gpa .deinit () == .ok ) catch @panic ("leak" );
10971084 const allocator = gpa .allocator ();
10981085
10991086 try std .testing .expectError (error .OutOfMemory , allocator .alloc (u8 , math .maxInt (usize )));
11001087}
11011088
11021089test "realloc" {
1103- var gpa = GeneralPurposeAllocator (test_config ){};
1090+ var gpa = DebugAllocator (test_config ){};
11041091 defer std .testing .expect (gpa .deinit () == .ok ) catch @panic ("leak" );
11051092 const allocator = gpa .allocator ();
11061093
@@ -1122,7 +1109,7 @@ test "realloc" {
11221109}
11231110
11241111test "shrink" {
1125- var gpa : GeneralPurposeAllocator (test_config ) = .{};
1112+ var gpa : DebugAllocator (test_config ) = .{};
11261113 defer std .testing .expect (gpa .deinit () == .ok ) catch @panic ("leak" );
11271114 const allocator = gpa .allocator ();
11281115
@@ -1147,7 +1134,7 @@ test "large object - grow" {
11471134 // Not expected to pass on targets that do not have memory mapping.
11481135 return error .SkipZigTest ;
11491136 }
1150- var gpa : GeneralPurposeAllocator (test_config ) = .{};
1137+ var gpa : DebugAllocator (test_config ) = .{};
11511138 defer std .testing .expect (gpa .deinit () == .ok ) catch @panic ("leak" );
11521139 const allocator = gpa .allocator ();
11531140
@@ -1165,7 +1152,7 @@ test "large object - grow" {
11651152}
11661153
11671154test "realloc small object to large object" {
1168- var gpa = GeneralPurposeAllocator (test_config ){};
1155+ var gpa = DebugAllocator (test_config ){};
11691156 defer std .testing .expect (gpa .deinit () == .ok ) catch @panic ("leak" );
11701157 const allocator = gpa .allocator ();
11711158
@@ -1182,7 +1169,7 @@ test "realloc small object to large object" {
11821169}
11831170
11841171test "shrink large object to large object" {
1185- var gpa : GeneralPurposeAllocator (test_config ) = .{};
1172+ var gpa : DebugAllocator (test_config ) = .{};
11861173 defer std .testing .expect (gpa .deinit () == .ok ) catch @panic ("leak" );
11871174 const allocator = gpa .allocator ();
11881175
@@ -1209,7 +1196,7 @@ test "shrink large object to large object" {
12091196test "shrink large object to large object with larger alignment" {
12101197 if (! builtin .link_libc and builtin .os .tag == .wasi ) return error .SkipZigTest ; // https://github.com/ziglang/zig/issues/22731
12111198
1212- var gpa = GeneralPurposeAllocator (test_config ){};
1199+ var gpa = DebugAllocator (test_config ){};
12131200 defer std .testing .expect (gpa .deinit () == .ok ) catch @panic ("leak" );
12141201 const allocator = gpa .allocator ();
12151202
@@ -1245,7 +1232,7 @@ test "shrink large object to large object with larger alignment" {
12451232}
12461233
12471234test "realloc large object to small object" {
1248- var gpa = GeneralPurposeAllocator (test_config ){};
1235+ var gpa = DebugAllocator (test_config ){};
12491236 defer std .testing .expect (gpa .deinit () == .ok ) catch @panic ("leak" );
12501237 const allocator = gpa .allocator ();
12511238
@@ -1260,7 +1247,7 @@ test "realloc large object to small object" {
12601247}
12611248
12621249test "overridable mutexes" {
1263- var gpa = GeneralPurposeAllocator (.{ .MutexType = std .Thread .Mutex }){
1250+ var gpa = DebugAllocator (.{ .MutexType = std .Thread .Mutex }){
12641251 .backing_allocator = std .testing .allocator ,
12651252 .mutex = std.Thread.Mutex {},
12661253 };
@@ -1272,7 +1259,11 @@ test "overridable mutexes" {
12721259}
12731260
12741261test "non-page-allocator backing allocator" {
1275- var gpa = GeneralPurposeAllocator (.{}){ .backing_allocator = std .testing .allocator };
1262+ var gpa : DebugAllocator (.{
1263+ .backing_allocator_zeroes = false ,
1264+ }) = .{
1265+ .backing_allocator = std .testing .allocator ,
1266+ };
12761267 defer std .testing .expect (gpa .deinit () == .ok ) catch @panic ("leak" );
12771268 const allocator = gpa .allocator ();
12781269
@@ -1283,7 +1274,7 @@ test "non-page-allocator backing allocator" {
12831274test "realloc large object to larger alignment" {
12841275 if (! builtin .link_libc and builtin .os .tag == .wasi ) return error .SkipZigTest ; // https://github.com/ziglang/zig/issues/22731
12851276
1286- var gpa = GeneralPurposeAllocator (test_config ){};
1277+ var gpa = DebugAllocator (test_config ){};
12871278 defer std .testing .expect (gpa .deinit () == .ok ) catch @panic ("leak" );
12881279 const allocator = gpa .allocator ();
12891280
@@ -1330,7 +1321,9 @@ test "large object rejects shrinking to small" {
13301321 }
13311322
13321323 var failing_allocator = std .testing .FailingAllocator .init (std .heap .page_allocator , .{ .fail_index = 3 });
1333- var gpa : GeneralPurposeAllocator (.{}) = .{ .backing_allocator = failing_allocator .allocator () };
1324+ var gpa : DebugAllocator (.{}) = .{
1325+ .backing_allocator = failing_allocator .allocator (),
1326+ };
13341327 defer std .testing .expect (gpa .deinit () == .ok ) catch @panic ("leak" );
13351328 const allocator = gpa .allocator ();
13361329
@@ -1345,7 +1338,7 @@ test "large object rejects shrinking to small" {
13451338}
13461339
13471340test "objects of size 1024 and 2048" {
1348- var gpa = GeneralPurposeAllocator (test_config ){};
1341+ var gpa = DebugAllocator (test_config ){};
13491342 defer std .testing .expect (gpa .deinit () == .ok ) catch @panic ("leak" );
13501343 const allocator = gpa .allocator ();
13511344
@@ -1357,7 +1350,7 @@ test "objects of size 1024 and 2048" {
13571350}
13581351
13591352test "setting a memory cap" {
1360- var gpa = GeneralPurposeAllocator (.{ .enable_memory_limit = true }){};
1353+ var gpa = DebugAllocator (.{ .enable_memory_limit = true }){};
13611354 defer std .testing .expect (gpa .deinit () == .ok ) catch @panic ("leak" );
13621355 const allocator = gpa .allocator ();
13631356
@@ -1383,7 +1376,7 @@ test "setting a memory cap" {
13831376}
13841377
13851378test "large allocations count requested size not backing size" {
1386- var gpa : GeneralPurposeAllocator (.{ .enable_memory_limit = true }) = .{};
1379+ var gpa : DebugAllocator (.{ .enable_memory_limit = true }) = .{};
13871380 const allocator = gpa .allocator ();
13881381
13891382 var buf = try allocator .alignedAlloc (u8 , 1 , page_size + 1 );
@@ -1395,7 +1388,7 @@ test "large allocations count requested size not backing size" {
13951388}
13961389
13971390test "retain metadata and never unmap" {
1398- var gpa = std .heap .GeneralPurposeAllocator (.{
1391+ var gpa = std .heap .DebugAllocator (.{
13991392 .safety = true ,
14001393 .never_unmap = true ,
14011394 .retain_metadata = true ,
0 commit comments