authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-01-29 15:24:24-08:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-01-29 21:10:20-08:00
logfecdc53a48970d55fc3ac1beb5be64b8589146cf
treec6400a237461c6f134dbc977eceae650de435b98
parentcd365b8b824efefb1948c4208722693646510cad

delete std.heap.WasmPageAllocator

This allocator has no purpose since it cannot truly fulfill the role of page allocation, and std.heap.wasm_allocator is better both in terms of performance and code size. This commit redefines `std.heap.page_allocator` to be less strict: "On operating systems that support memory mapping, this allocator makes a syscall directly for every allocation and free. Otherwise, it falls back to the preferred singleton for the target. Thread-safe." This now matches how it was actually being implemented, and matches its use sites - which are mainly as the backing allocator for `std.heap.ArenaAllocator`.

4 files changed, 34 insertions(+), 258 deletions(-)

lib/std/heap.zig+19-22
...@@ -18,7 +18,6 @@ pub const GeneralPurposeAllocatorConfig = @import("heap/general_purpose_allocato...@@ -18,7 +18,6 @@ pub const GeneralPurposeAllocatorConfig = @import("heap/general_purpose_allocato
18pub const GeneralPurposeAllocator = @import("heap/general_purpose_allocator.zig").GeneralPurposeAllocator;18pub const GeneralPurposeAllocator = @import("heap/general_purpose_allocator.zig").GeneralPurposeAllocator;
19pub const Check = @import("heap/general_purpose_allocator.zig").Check;19pub const Check = @import("heap/general_purpose_allocator.zig").Check;
20pub const WasmAllocator = @import("heap/WasmAllocator.zig");20pub const WasmAllocator = @import("heap/WasmAllocator.zig");
21pub const WasmPageAllocator = @import("heap/WasmPageAllocator.zig");
22pub const PageAllocator = @import("heap/PageAllocator.zig");21pub const PageAllocator = @import("heap/PageAllocator.zig");
23pub const ThreadSafeAllocator = @import("heap/ThreadSafeAllocator.zig");22pub const ThreadSafeAllocator = @import("heap/ThreadSafeAllocator.zig");
24pub const SbrkAllocator = @import("heap/sbrk_allocator.zig").SbrkAllocator;23pub const SbrkAllocator = @import("heap/sbrk_allocator.zig").SbrkAllocator;
...@@ -223,36 +222,35 @@ fn rawCFree(...@@ -223,36 +222,35 @@ fn rawCFree(
223 c.free(buf.ptr);222 c.free(buf.ptr);
224}223}
225224
226/// This allocator makes a syscall directly for every allocation and free.225/// On operating systems that support memory mapping, this allocator makes a
227/// Thread-safe and lock-free.226/// syscall directly for every allocation and free.
228pub const page_allocator = if (@hasDecl(root, "os") and227///
228/// Otherwise, it falls back to the preferred singleton for the target.
229///
230/// Thread-safe.
231pub const page_allocator: Allocator = if (@hasDecl(root, "os") and
229 @hasDecl(root.os, "heap") and232 @hasDecl(root.os, "heap") and
230 @hasDecl(root.os.heap, "page_allocator"))233 @hasDecl(root.os.heap, "page_allocator"))
231 root.os.heap.page_allocator234 root.os.heap.page_allocator
232else if (builtin.target.isWasm())235else if (builtin.target.isWasm()) .{
233 Allocator{236 .ptr = undefined,
234 .ptr = undefined,237 .vtable = &WasmAllocator.vtable,
235 .vtable = &WasmPageAllocator.vtable,238} else if (builtin.target.os.tag == .plan9) .{
236 }239 .ptr = undefined,
237else if (builtin.target.os.tag == .plan9)240 .vtable = &SbrkAllocator(std.os.plan9.sbrk).vtable,
238 Allocator{241} else .{
239 .ptr = undefined,242 .ptr = undefined,
240 .vtable = &SbrkAllocator(std.os.plan9.sbrk).vtable,243 .vtable = &PageAllocator.vtable,
241 }244};
242else
243 Allocator{
244 .ptr = undefined,
245 .vtable = &PageAllocator.vtable,
246 };
247245
248/// This allocator is fast, small, and specific to WebAssembly. In the future,246/// This allocator is fast, small, and specific to WebAssembly. In the future,
249/// this will be the implementation automatically selected by247/// this will be the implementation automatically selected by
250/// `GeneralPurposeAllocator` when compiling in `ReleaseSmall` mode for wasm32248/// `GeneralPurposeAllocator` when compiling in `ReleaseSmall` mode for wasm32
251/// and wasm64 architectures.249/// and wasm64 architectures.
252/// Until then, it is available here to play with.250/// Until then, it is available here to play with.
253pub const wasm_allocator = Allocator{251pub const wasm_allocator: Allocator = .{
254 .ptr = undefined,252 .ptr = undefined,
255 .vtable = &std.heap.WasmAllocator.vtable,253 .vtable = &WasmAllocator.vtable,
256};254};
257255
258/// Verifies that the adjusted length will still map to the full length256/// Verifies that the adjusted length will still map to the full length
...@@ -892,6 +890,5 @@ test {...@@ -892,6 +890,5 @@ test {
892 _ = GeneralPurposeAllocator;890 _ = GeneralPurposeAllocator;
893 if (builtin.target.isWasm()) {891 if (builtin.target.isWasm()) {
894 _ = WasmAllocator;892 _ = WasmAllocator;
895 _ = WasmPageAllocator;
896 }893 }
897}894}
lib/std/heap/WasmAllocator.zig+5-2
...@@ -10,11 +10,14 @@ const math = std.math;...@@ -10,11 +10,14 @@ const math = std.math;
1010
11comptime {11comptime {
12 if (!builtin.target.isWasm()) {12 if (!builtin.target.isWasm()) {
13 @compileError("WasmPageAllocator is only available for wasm32 arch");13 @compileError("only available for wasm32 arch");
14 }
15 if (!builtin.single_threaded) {
16 @compileError("TODO implement support for multi-threaded wasm");
14 }17 }
15}18}
1619
17pub const vtable = Allocator.VTable{20pub const vtable: Allocator.VTable = .{
18 .alloc = alloc,21 .alloc = alloc,
19 .resize = resize,22 .resize = resize,
20 .free = free,23 .free = free,
lib/std/heap/WasmPageAllocator.zig deleted-233
...@@ -1,233 +0,0 @@
1const WasmPageAllocator = @This();
2const std = @import("../std.zig");
3const builtin = @import("builtin");
4const Allocator = std.mem.Allocator;
5const mem = std.mem;
6const maxInt = std.math.maxInt;
7const assert = std.debug.assert;
8
9comptime {
10 if (!builtin.target.isWasm()) {
11 @compileError("WasmPageAllocator is only available for wasm32 arch");
12 }
13}
14
15pub const vtable = Allocator.VTable{
16 .alloc = alloc,
17 .resize = resize,
18 .free = free,
19};
20
21const PageStatus = enum(u1) {
22 used = 0,
23 free = 1,
24
25 pub const none_free: u8 = 0;
26};
27
28const FreeBlock = struct {
29 data: []u128,
30
31 fn totalPages(self: FreeBlock) usize {
32 return self.data.len * 128;
33 }
34
35 fn isInitialized(self: FreeBlock) bool {
36 return self.data.len > 0;
37 }
38
39 fn getBit(self: FreeBlock, idx: usize) PageStatus {
40 const bit = mem.readPackedInt(u1, mem.sliceAsBytes(self.data), idx, .little);
41 return @as(PageStatus, @enumFromInt(bit));
42 }
43
44 fn setBits(self: FreeBlock, start_idx: usize, len: usize, val: PageStatus) void {
45 var i: usize = 0;
46 const bytes = mem.sliceAsBytes(self.data);
47 while (i < len) : (i += 1) {
48 mem.writePackedInt(u1, bytes, start_idx + i, @intFromEnum(val), .little);
49 }
50 }
51
52 // Use '0xFFFFFFFF' as a _missing_ sentinel
53 // This saves ~50 bytes compared to returning a nullable
54
55 // We can guarantee that conventional memory never gets this big,
56 // and wasm32 would not be able to address this memory (32 GB > usize).
57
58 // Revisit if this is settled: https://github.com/ziglang/zig/issues/3806
59 const not_found = maxInt(usize);
60
61 fn useRecycled(self: FreeBlock, num_pages: usize, log2_align: u8) usize {
62 @branchHint(.cold);
63 for (self.data, 0..) |segment, i| {
64 const spills_into_next = @as(i128, @bitCast(segment)) < 0;
65 const has_enough_bits = @popCount(segment) >= num_pages;
66
67 if (!spills_into_next and !has_enough_bits) continue;
68
69 var j: usize = i * 128;
70 while (j < (i + 1) * 128) : (j += 1) {
71 var count: usize = 0;
72 while (j + count < self.totalPages() and self.getBit(j + count) == .free) {
73 count += 1;
74 const addr = j * mem.page_size;
75 if (count >= num_pages and mem.isAlignedLog2(addr, log2_align)) {
76 self.setBits(j, num_pages, .used);
77 return j;
78 }
79 }
80 j += count;
81 }
82 }
83 return not_found;
84 }
85
86 fn recycle(self: FreeBlock, start_idx: usize, len: usize) void {
87 self.setBits(start_idx, len, .free);
88 }
89};
90
91var _conventional_data = [_]u128{0} ** 16;
92// Marking `conventional` as const saves ~40 bytes
93const conventional = FreeBlock{ .data = &_conventional_data };
94var extended = FreeBlock{ .data = &[_]u128{} };
95
96fn extendedOffset() usize {
97 return conventional.totalPages();
98}
99
100fn nPages(memsize: usize) usize {
101 return mem.alignForward(usize, memsize, mem.page_size) / mem.page_size;
102}
103
104fn alloc(ctx: *anyopaque, len: usize, log2_align: u8, ra: usize) ?[*]u8 {
105 _ = ctx;
106 _ = ra;
107 if (len > maxInt(usize) - (mem.page_size - 1)) return null;
108 const page_count = nPages(len);
109 const page_idx = allocPages(page_count, log2_align) catch return null;
110 return @as([*]u8, @ptrFromInt(page_idx * mem.page_size));
111}
112
113fn allocPages(page_count: usize, log2_align: u8) !usize {
114 {
115 const idx = conventional.useRecycled(page_count, log2_align);
116 if (idx != FreeBlock.not_found) {
117 return idx;
118 }
119 }
120
121 const idx = extended.useRecycled(page_count, log2_align);
122 if (idx != FreeBlock.not_found) {
123 return idx + extendedOffset();
124 }
125
126 const next_page_idx = @wasmMemorySize(0);
127 const next_page_addr = next_page_idx * mem.page_size;
128 const aligned_addr = mem.alignForwardLog2(next_page_addr, log2_align);
129 const drop_page_count = @divExact(aligned_addr - next_page_addr, mem.page_size);
130 const result = @wasmMemoryGrow(0, @as(u32, @intCast(drop_page_count + page_count)));
131 if (result <= 0)
132 return error.OutOfMemory;
133 assert(result == next_page_idx);
134 const aligned_page_idx = next_page_idx + drop_page_count;
135 if (drop_page_count > 0) {
136 freePages(next_page_idx, aligned_page_idx);
137 }
138 return @as(usize, @intCast(aligned_page_idx));
139}
140
141fn freePages(start: usize, end: usize) void {
142 if (start < extendedOffset()) {
143 conventional.recycle(start, @min(extendedOffset(), end) - start);
144 }
145 if (end > extendedOffset()) {
146 var new_end = end;
147 if (!extended.isInitialized()) {
148 // Steal the last page from the memory currently being recycled
149 // TODO: would it be better if we use the first page instead?
150 new_end -= 1;
151
152 extended.data = @as([*]u128, @ptrFromInt(new_end * mem.page_size))[0 .. mem.page_size / @sizeOf(u128)];
153 // Since this is the first page being freed and we consume it, assume *nothing* is free.
154 @memset(extended.data, PageStatus.none_free);
155 }
156 const clamped_start = @max(extendedOffset(), start);
157 extended.recycle(clamped_start - extendedOffset(), new_end - clamped_start);
158 }
159}
160
161fn resize(
162 ctx: *anyopaque,
163 buf: []u8,
164 log2_buf_align: u8,
165 new_len: usize,
166 return_address: usize,
167) bool {
168 _ = ctx;
169 _ = log2_buf_align;
170 _ = return_address;
171 const aligned_len = mem.alignForward(usize, buf.len, mem.page_size);
172 if (new_len > aligned_len) return false;
173 const current_n = nPages(aligned_len);
174 const new_n = nPages(new_len);
175 if (new_n != current_n) {
176 const base = nPages(@intFromPtr(buf.ptr));
177 freePages(base + new_n, base + current_n);
178 }
179 return true;
180}
181
182fn free(
183 ctx: *anyopaque,
184 buf: []u8,
185 log2_buf_align: u8,
186 return_address: usize,
187) void {
188 _ = ctx;
189 _ = log2_buf_align;
190 _ = return_address;
191 const aligned_len = mem.alignForward(usize, buf.len, mem.page_size);
192 const current_n = nPages(aligned_len);
193 const base = nPages(@intFromPtr(buf.ptr));
194 freePages(base, base + current_n);
195}
196
197test "internals" {
198 const page_allocator = std.heap.page_allocator;
199 const testing = std.testing;
200
201 const conventional_memsize = WasmPageAllocator.conventional.totalPages() * mem.page_size;
202 const initial = try page_allocator.alloc(u8, mem.page_size);
203 try testing.expect(@intFromPtr(initial.ptr) < conventional_memsize); // If this isn't conventional, the rest of these tests don't make sense. Also we have a serious memory leak in the test suite.
204
205 var inplace = try page_allocator.realloc(initial, 1);
206 try testing.expectEqual(initial.ptr, inplace.ptr);
207 inplace = try page_allocator.realloc(inplace, 4);
208 try testing.expectEqual(initial.ptr, inplace.ptr);
209 page_allocator.free(inplace);
210
211 const reuse = try page_allocator.alloc(u8, 1);
212 try testing.expectEqual(initial.ptr, reuse.ptr);
213 page_allocator.free(reuse);
214
215 // This segment may span conventional and extended which has really complex rules so we're just ignoring it for now.
216 const padding = try page_allocator.alloc(u8, conventional_memsize);
217 page_allocator.free(padding);
218
219 const ext = try page_allocator.alloc(u8, conventional_memsize);
220 try testing.expect(@intFromPtr(ext.ptr) >= conventional_memsize);
221
222 const use_small = try page_allocator.alloc(u8, 1);
223 try testing.expectEqual(initial.ptr, use_small.ptr);
224 page_allocator.free(use_small);
225
226 inplace = try page_allocator.realloc(ext, 1);
227 try testing.expectEqual(ext.ptr, inplace.ptr);
228 page_allocator.free(inplace);
229
230 const reuse_extended = try page_allocator.alloc(u8, conventional_memsize);
231 try testing.expectEqual(ext.ptr, reuse_extended.ptr);
232 page_allocator.free(reuse_extended);
233}
lib/std/heap/general_purpose_allocator.zig+10-1
...@@ -1171,7 +1171,11 @@ test "shrink" {...@@ -1171,7 +1171,11 @@ test "shrink" {
1171}1171}
11721172
1173test "large object - grow" {1173test "large object - grow" {
1174 var gpa = GeneralPurposeAllocator(test_config){};1174 if (builtin.target.isWasm()) {
1175 // Not expected to pass on targets that do not have memory mapping.
1176 return error.SkipZigTest;
1177 }
1178 var gpa: GeneralPurposeAllocator(test_config) = .{};
1175 defer std.testing.expect(gpa.deinit() == .ok) catch @panic("leak");1179 defer std.testing.expect(gpa.deinit() == .ok) catch @panic("leak");
1176 const allocator = gpa.allocator();1180 const allocator = gpa.allocator();
11771181
...@@ -1344,6 +1348,11 @@ test "realloc large object to larger alignment" {...@@ -1344,6 +1348,11 @@ test "realloc large object to larger alignment" {
1344}1348}
13451349
1346test "large object shrinks to small but allocation fails during shrink" {1350test "large object shrinks to small but allocation fails during shrink" {
1351 if (builtin.target.isWasm()) {
1352 // Not expected to pass on targets that do not have memory mapping.
1353 return error.SkipZigTest;
1354 }
1355
1347 var failing_allocator = std.testing.FailingAllocator.init(std.heap.page_allocator, .{ .fail_index = 3 });1356 var failing_allocator = std.testing.FailingAllocator.init(std.heap.page_allocator, .{ .fail_index = 3 });
1348 var gpa = GeneralPurposeAllocator(.{}){ .backing_allocator = failing_allocator.allocator() };1357 var gpa = GeneralPurposeAllocator(.{}){ .backing_allocator = failing_allocator.allocator() };
1349 defer std.testing.expect(gpa.deinit() == .ok) catch @panic("leak");1358 defer std.testing.expect(gpa.deinit() == .ok) catch @panic("leak");