authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-02-06 03:31:32-08:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-02-07 12:20:12-08:00
log51c4ffa410d2cf51b7b45dab4dfd033db2190b7e
treea7a0427a2cf070a4b96b9a326d9c1c7d0e8555ee
parent6a6e72fff820fb641aa1b00700f6835430dae72e

add std.heap.SmpAllocator

An allocator intended to be used in -OReleaseFast mode when multi-threading is enabled.

5 files changed, 316 insertions(+), 19 deletions(-)

lib/std/heap.zig+10-3
......@@ -9,11 +9,12 @@ const Allocator = std.mem.Allocator;
99const windows = std.os.windows;
1010
1111pub const ArenaAllocator = @import("heap/arena_allocator.zig").ArenaAllocator;
12pub const WasmAllocator = @import("heap/WasmAllocator.zig");
12pub const SmpAllocator = @import("heap/SmpAllocator.zig");
13pub const FixedBufferAllocator = @import("heap/FixedBufferAllocator.zig");
1314pub const PageAllocator = @import("heap/PageAllocator.zig");
14pub const ThreadSafeAllocator = @import("heap/ThreadSafeAllocator.zig");
1515pub const SbrkAllocator = @import("heap/sbrk_allocator.zig").SbrkAllocator;
16pub const FixedBufferAllocator = @import("heap/FixedBufferAllocator.zig");
16pub const ThreadSafeAllocator = @import("heap/ThreadSafeAllocator.zig");
17pub const WasmAllocator = @import("heap/WasmAllocator.zig");
1718
1819pub const DebugAllocatorConfig = @import("heap/debug_allocator.zig").Config;
1920pub const DebugAllocator = @import("heap/debug_allocator.zig").DebugAllocator;
......@@ -358,6 +359,11 @@ else if (builtin.target.isWasm()) .{
358359 .vtable = &PageAllocator.vtable,
359360};
360361
362pub const smp_allocator: Allocator = .{
363 .ptr = undefined,
364 .vtable = &SmpAllocator.vtable,
365};
366
361367/// This allocator is fast, small, and specific to WebAssembly. In the future,
362368/// this will be the implementation automatically selected by
363369/// `GeneralPurposeAllocator` when compiling in `ReleaseSmall` mode for wasm32
......@@ -978,4 +984,5 @@ test {
978984 if (builtin.target.isWasm()) {
979985 _ = WasmAllocator;
980986 }
987 if (!builtin.single_threaded) _ = smp_allocator;
981988}
lib/std/heap/PageAllocator.zig+18-12
......@@ -16,11 +16,7 @@ pub const vtable: Allocator.VTable = .{
1616 .free = free,
1717};
1818
19fn alloc(context: *anyopaque, n: usize, alignment: mem.Alignment, ra: usize) ?[*]u8 {
20 _ = context;
21 _ = ra;
22 assert(n > 0);
23
19pub fn map(n: usize, alignment: mem.Alignment) ?[*]u8 {
2420 const page_size = std.heap.pageSize();
2521 if (n >= maxInt(usize) - page_size) return null;
2622 const alignment_bytes = alignment.toByteUnits();
......@@ -101,6 +97,13 @@ fn alloc(context: *anyopaque, n: usize, alignment: mem.Alignment, ra: usize) ?[*
10197 return result_ptr;
10298}
10399
100fn alloc(context: *anyopaque, n: usize, alignment: mem.Alignment, ra: usize) ?[*]u8 {
101 _ = context;
102 _ = ra;
103 assert(n > 0);
104 return map(n, alignment);
105}
106
104107fn resize(
105108 context: *anyopaque,
106109 memory: []u8,
......@@ -114,7 +117,7 @@ fn resize(
114117 return realloc(memory, new_len, false) != null;
115118}
116119
117pub fn remap(
120fn remap(
118121 context: *anyopaque,
119122 memory: []u8,
120123 alignment: mem.Alignment,
......@@ -127,21 +130,24 @@ pub fn remap(
127130 return realloc(memory, new_len, true);
128131}
129132
130fn free(context: *anyopaque, slice: []u8, alignment: mem.Alignment, return_address: usize) void {
133fn free(context: *anyopaque, memory: []u8, alignment: mem.Alignment, return_address: usize) void {
131134 _ = context;
132135 _ = alignment;
133136 _ = return_address;
137 return unmap(@alignCast(memory));
138}
134139
140pub fn unmap(memory: []align(page_size_min) u8) void {
135141 if (native_os == .windows) {
136 windows.VirtualFree(slice.ptr, 0, windows.MEM_RELEASE);
142 windows.VirtualFree(memory.ptr, 0, windows.MEM_RELEASE);
137143 } else {
138 const buf_aligned_len = mem.alignForward(usize, slice.len, std.heap.pageSize());
139 posix.munmap(@alignCast(slice.ptr[0..buf_aligned_len]));
144 const page_aligned_len = mem.alignForward(usize, memory.len, std.heap.pageSize());
145 posix.munmap(memory.ptr[0..page_aligned_len]);
140146 }
141147}
142148
143fn realloc(uncasted_memory: []u8, new_len: usize, may_move: bool) ?[*]u8 {
144 const memory: []align(std.heap.page_size_min) u8 = @alignCast(uncasted_memory);
149pub fn realloc(uncasted_memory: []u8, new_len: usize, may_move: bool) ?[*]u8 {
150 const memory: []align(page_size_min) u8 = @alignCast(uncasted_memory);
145151 const page_size = std.heap.pageSize();
146152 const new_size_aligned = mem.alignForward(usize, new_len, page_size);
147153
lib/std/heap/SmpAllocator.zig created+288
......@@ -0,0 +1,288 @@
1//! An allocator that is designed for ReleaseFast optimization mode, with
2//! multi-threading enabled.
3//!
4//! This allocator is a singleton; it uses global state and only one should be
5//! instantiated for the entire process.
6//!
7//! ## Basic Design
8//!
9//! Avoid locking the global mutex as much as possible.
10//!
11//! Each thread gets a separate freelist, however, the data must be recoverable
12//! when the thread exits. We do not directly learn when a thread exits, so
13//! occasionally, one thread must attempt to reclaim another thread's
14//! resources.
15//!
16//! Above a certain size, those allocations are memory mapped directly, with no
17//! storage of allocation metadata. This works because the implementation
18//! refuses resizes that would move an allocation from small category to large
19//! category or vice versa.
20//!
21//! Each allocator operation checks the thread identifier from a threadlocal
22//! variable to find out which metadata in the global state to access, and
23//! attempts to grab its lock. This will usually succeed without contention,
24//! unless another thread has been assigned the same id. In the case of such
25//! contention, the thread moves on to the next thread metadata slot and
26//! repeats the process of attempting to obtain the lock.
27//!
28//! By limiting the thread-local metadata array to the same number as the CPU
29//! count, ensures that as threads are created and destroyed, they cycle
30//! through the full set of freelists.
31
32const builtin = @import("builtin");
33const native_os = builtin.os.tag;
34
35const std = @import("../std.zig");
36const assert = std.debug.assert;
37const mem = std.mem;
38const math = std.math;
39const Allocator = std.mem.Allocator;
40const SmpAllocator = @This();
41const PageAllocator = std.heap.PageAllocator;
42
43/// Protects the state in this struct (global state), except for `threads`
44/// which each have their own mutex.
45mutex: std.Thread.Mutex,
46next_thread_index: u32,
47cpu_count: u32,
48threads: [max_thread_count]Thread,
49
50var global: SmpAllocator = .{
51 .mutex = .{},
52 .next_thread_index = 0,
53 .threads = @splat(.{}),
54 .cpu_count = 0,
55};
56threadlocal var thread_id: Thread.Id = .none;
57
58const max_thread_count = 128;
59const slab_len: usize = @max(std.heap.page_size_max, switch (builtin.os.tag) {
60 .windows => 64 * 1024, // Makes `std.heap.PageAllocator` take the happy path.
61 .wasi => 64 * 1024, // Max alignment supported by `std.heap.WasmAllocator`.
62 else => 256 * 1024, // Avoids too many active mappings when `page_size_max` is low.
63});
64/// Because of storing free list pointers, the minimum size class is 3.
65const min_class = math.log2(math.ceilPowerOfTwoAssert(usize, 1 + @sizeOf(usize)));
66const size_class_count = math.log2(slab_len) - min_class;
67
68const Thread = struct {
69 /// Avoid false sharing.
70 _: void align(std.atomic.cache_line) = {},
71
72 /// Protects the state in this struct (per-thread state).
73 ///
74 /// Threads lock this before accessing their own state in order
75 /// to support freelist reclamation.
76 mutex: std.Thread.Mutex = .{},
77
78 next_addrs: [size_class_count]usize = @splat(0),
79 /// For each size class, points to the freed pointer.
80 frees: [size_class_count]usize = @splat(0),
81
82 /// Index into `SmpAllocator.threads`.
83 const Id = enum(usize) {
84 none = 0,
85 first = 1,
86 _,
87
88 fn fromIndex(index: usize) Id {
89 return @enumFromInt(index + 1);
90 }
91
92 fn toIndex(id: Id) usize {
93 return @intFromEnum(id) - 1;
94 }
95 };
96
97 fn lock() *Thread {
98 const id = thread_id;
99 if (id != .none) {
100 var index = id.toIndex();
101 {
102 const t = &global.threads[index];
103 if (t.mutex.tryLock()) return t;
104 }
105 const cpu_count = global.cpu_count;
106 assert(cpu_count != 0);
107 while (true) {
108 index = (index + 1) % cpu_count;
109 const t = &global.threads[index];
110 if (t.mutex.tryLock()) {
111 thread_id = .fromIndex(index);
112 return t;
113 }
114 }
115 }
116 while (true) {
117 const thread_index = i: {
118 global.mutex.lock();
119 defer global.mutex.unlock();
120 const cpu_count = c: {
121 const cpu_count = global.cpu_count;
122 if (cpu_count == 0) {
123 const n: u32 = @intCast(@max(std.Thread.getCpuCount() catch max_thread_count, max_thread_count));
124 global.cpu_count = n;
125 break :c n;
126 }
127 break :c cpu_count;
128 };
129 const thread_index = global.next_thread_index;
130 global.next_thread_index = @intCast((thread_index + 1) % cpu_count);
131 break :i thread_index;
132 };
133 const t = &global.threads[thread_index];
134 if (t.mutex.tryLock()) {
135 thread_id = .fromIndex(thread_index);
136 return t;
137 }
138 }
139 }
140
141 fn unlock(t: *Thread) void {
142 t.mutex.unlock();
143 }
144};
145
146pub const vtable: Allocator.VTable = .{
147 .alloc = alloc,
148 .resize = resize,
149 .remap = remap,
150 .free = free,
151};
152
153comptime {
154 assert(!builtin.single_threaded); // you're holding it wrong
155}
156
157fn alloc(context: *anyopaque, len: usize, alignment: mem.Alignment, ra: usize) ?[*]u8 {
158 _ = context;
159 _ = ra;
160 const class = sizeClassIndex(len, alignment);
161 if (class >= size_class_count) {
162 @branchHint(.unlikely);
163 return PageAllocator.map(len, alignment);
164 }
165
166 const t = Thread.lock();
167 defer t.unlock();
168
169 const slot_size = slotSize(class);
170
171 const top_free_ptr = t.frees[class];
172 if (top_free_ptr != 0) {
173 const node: *usize = @ptrFromInt(top_free_ptr + (slot_size - @sizeOf(usize)));
174 t.frees[class] = node.*;
175 return @ptrFromInt(top_free_ptr);
176 }
177
178 const next_addr = t.next_addrs[class];
179 if (next_addr % slab_len == 0) {
180 const slab = PageAllocator.map(slab_len, .fromByteUnits(std.heap.pageSize())) orelse return null;
181 t.next_addrs[class] = @intFromPtr(slab) + slot_size;
182 return slab;
183 }
184
185 t.next_addrs[class] = next_addr + slot_size;
186 return @ptrFromInt(next_addr);
187}
188
189fn resize(context: *anyopaque, memory: []u8, alignment: mem.Alignment, new_len: usize, ra: usize) bool {
190 _ = context;
191 _ = ra;
192 const class = sizeClassIndex(memory.len, alignment);
193 const new_class = sizeClassIndex(new_len, alignment);
194 if (class >= size_class_count) {
195 if (new_class < size_class_count) return false;
196 return PageAllocator.realloc(memory, new_len, false) != null;
197 }
198 return new_class == class;
199}
200
201fn remap(context: *anyopaque, memory: []u8, alignment: mem.Alignment, new_len: usize, ra: usize) ?[*]u8 {
202 _ = context;
203 _ = ra;
204 const class = sizeClassIndex(memory.len, alignment);
205 const new_class = sizeClassIndex(new_len, alignment);
206 if (class >= size_class_count) {
207 if (new_class < size_class_count) return null;
208 return PageAllocator.realloc(memory, new_len, true);
209 }
210 return if (new_class == class) memory.ptr else null;
211}
212
213fn free(context: *anyopaque, memory: []u8, alignment: mem.Alignment, ra: usize) void {
214 _ = context;
215 _ = ra;
216 const class = sizeClassIndex(memory.len, alignment);
217 if (class >= size_class_count) {
218 @branchHint(.unlikely);
219 return PageAllocator.unmap(@alignCast(memory));
220 }
221
222 const t = Thread.lock();
223 defer t.unlock();
224
225 const slot_size = slotSize(class);
226 const addr = @intFromPtr(memory.ptr);
227 const node: *usize = @ptrFromInt(addr + (slot_size - @sizeOf(usize)));
228 node.* = t.frees[class];
229 t.frees[class] = addr;
230}
231
232fn sizeClassIndex(len: usize, alignment: mem.Alignment) usize {
233 return @max(
234 @bitSizeOf(usize) - @clz(len - 1),
235 @intFromEnum(alignment),
236 min_class,
237 );
238}
239
240fn slotSize(class: usize) usize {
241 const Log2USize = std.math.Log2Int(usize);
242 return @as(usize, 1) << @as(Log2USize, @intCast(class));
243}
244
245test "large alloc, resize, remap, free" {
246 const gpa = std.heap.smp_allocator;
247
248 const ptr1 = try gpa.alloc(u64, 42768);
249 const ptr2 = try gpa.alloc(u64, 52768);
250 gpa.free(ptr1);
251 const ptr3 = try gpa.alloc(u64, 62768);
252 gpa.free(ptr3);
253 gpa.free(ptr2);
254}
255
256test "small allocations - free in same order" {
257 const gpa = std.heap.smp_allocator;
258
259 var list = std.ArrayList(*u64).init(std.testing.allocator);
260 defer list.deinit();
261
262 var i: usize = 0;
263 while (i < 513) : (i += 1) {
264 const ptr = try gpa.create(u64);
265 try list.append(ptr);
266 }
267
268 for (list.items) |ptr| {
269 gpa.destroy(ptr);
270 }
271}
272
273test "small allocations - free in reverse order" {
274 const gpa = std.heap.smp_allocator;
275
276 var list = std.ArrayList(*u64).init(std.testing.allocator);
277 defer list.deinit();
278
279 var i: usize = 0;
280 while (i < 513) : (i += 1) {
281 const ptr = try gpa.create(u64);
282 try list.append(ptr);
283 }
284
285 while (list.popOrNull()) |ptr| {
286 gpa.destroy(ptr);
287 }
288}
lib/std/heap/WasmAllocator.zig-2
......@@ -1,5 +1,3 @@
1//! This is intended to be merged into GeneralPurposeAllocator at some point.
2
31const std = @import("../std.zig");
42const builtin = @import("builtin");
53const Allocator = std.mem.Allocator;
lib/std/heap/debug_allocator.zig-2
......@@ -851,8 +851,6 @@ pub fn DebugAllocator(comptime config: Config) type {
851851 self.mutex.lock();
852852 defer self.mutex.unlock();
853853
854 assert(old_memory.len != 0);
855
856854 const size_class_index: usize = @max(@bitSizeOf(usize) - @clz(old_memory.len - 1), @intFromEnum(alignment));
857855 if (size_class_index >= self.buckets.len) {
858856 @branchHint(.unlikely);