authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2022-11-10 15:31:41-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2022-11-29 23:46:02-07:00
log9f8c19210b73704f67b64439c76837ce698cd1af
tree6311257663bf2ec0711490f552c2026859826c01
parente35f297aeb993ec956ae80379ddf7f86069e109b

std.heap: extract PageAllocator, WasmPageAllocator


3 files changed, 315 insertions(+), 303 deletions(-)

lib/std/heap.zig+12-303
......@@ -1,13 +1,13 @@
11const std = @import("std.zig");
22const builtin = @import("builtin");
33const root = @import("root");
4const debug = std.debug;
5const assert = debug.assert;
4const assert = std.debug.assert;
65const testing = std.testing;
76const mem = std.mem;
87const os = std.os;
98const c = std.c;
109const maxInt = std.math.maxInt;
10const Allocator = std.mem.Allocator;
1111
1212pub const LoggingAllocator = @import("heap/logging_allocator.zig").LoggingAllocator;
1313pub const loggingAllocator = @import("heap/logging_allocator.zig").loggingAllocator;
......@@ -16,8 +16,11 @@ pub const LogToWriterAllocator = @import("heap/log_to_writer_allocator.zig").Log
1616pub const logToWriterAllocator = @import("heap/log_to_writer_allocator.zig").logToWriterAllocator;
1717pub const ArenaAllocator = @import("heap/arena_allocator.zig").ArenaAllocator;
1818pub const GeneralPurposeAllocator = @import("heap/general_purpose_allocator.zig").GeneralPurposeAllocator;
19pub const WasmPageAllocator = @import("heap/WasmPageAllocator.zig");
20pub const PageAllocator = @import("heap/PageAllocator.zig");
1921
20const Allocator = mem.Allocator;
22/// TODO Utilize this on Windows.
23pub var next_mmap_addr_hint: ?[*]align(mem.page_size) u8 = null;
2124
2225const CAllocator = struct {
2326 comptime {
......@@ -227,303 +230,6 @@ pub fn alignPageAllocLen(full_len: usize, len: usize) usize {
227230 return aligned_len;
228231}
229232
230/// TODO Utilize this on Windows.
231pub var next_mmap_addr_hint: ?[*]align(mem.page_size) u8 = null;
232
233const PageAllocator = struct {
234 const vtable = Allocator.VTable{
235 .alloc = alloc,
236 .resize = resize,
237 .free = free,
238 };
239
240 fn alloc(_: *anyopaque, n: usize, log2_align: u8, ra: usize) ?[*]u8 {
241 _ = ra;
242 _ = log2_align;
243 assert(n > 0);
244 if (n > maxInt(usize) - (mem.page_size - 1)) return null;
245 const aligned_len = mem.alignForward(n, mem.page_size);
246
247 if (builtin.os.tag == .windows) {
248 const w = os.windows;
249 const addr = w.VirtualAlloc(
250 null,
251 aligned_len,
252 w.MEM_COMMIT | w.MEM_RESERVE,
253 w.PAGE_READWRITE,
254 ) catch return null;
255 return @ptrCast([*]align(mem.page_size) u8, @alignCast(mem.page_size, addr));
256 }
257
258 const hint = @atomicLoad(@TypeOf(next_mmap_addr_hint), &next_mmap_addr_hint, .Unordered);
259 const slice = os.mmap(
260 hint,
261 aligned_len,
262 os.PROT.READ | os.PROT.WRITE,
263 os.MAP.PRIVATE | os.MAP.ANONYMOUS,
264 -1,
265 0,
266 ) catch return null;
267 assert(mem.isAligned(@ptrToInt(slice.ptr), mem.page_size));
268 const new_hint = @alignCast(mem.page_size, slice.ptr + aligned_len);
269 _ = @cmpxchgStrong(@TypeOf(next_mmap_addr_hint), &next_mmap_addr_hint, hint, new_hint, .Monotonic, .Monotonic);
270 return slice.ptr;
271 }
272
273 fn resize(
274 _: *anyopaque,
275 buf_unaligned: []u8,
276 log2_buf_align: u8,
277 new_size: usize,
278 return_address: usize,
279 ) bool {
280 _ = log2_buf_align;
281 _ = return_address;
282 const new_size_aligned = mem.alignForward(new_size, mem.page_size);
283
284 if (builtin.os.tag == .windows) {
285 const w = os.windows;
286 if (new_size <= buf_unaligned.len) {
287 const base_addr = @ptrToInt(buf_unaligned.ptr);
288 const old_addr_end = base_addr + buf_unaligned.len;
289 const new_addr_end = mem.alignForward(base_addr + new_size, mem.page_size);
290 if (old_addr_end > new_addr_end) {
291 // For shrinking that is not releasing, we will only
292 // decommit the pages not needed anymore.
293 w.VirtualFree(
294 @intToPtr(*anyopaque, new_addr_end),
295 old_addr_end - new_addr_end,
296 w.MEM_DECOMMIT,
297 );
298 }
299 return true;
300 }
301 const old_size_aligned = mem.alignForward(buf_unaligned.len, mem.page_size);
302 if (new_size_aligned <= old_size_aligned) {
303 return true;
304 }
305 return false;
306 }
307
308 const buf_aligned_len = mem.alignForward(buf_unaligned.len, mem.page_size);
309 if (new_size_aligned == buf_aligned_len)
310 return true;
311
312 if (new_size_aligned < buf_aligned_len) {
313 const ptr = @alignCast(mem.page_size, buf_unaligned.ptr + new_size_aligned);
314 // TODO: if the next_mmap_addr_hint is within the unmapped range, update it
315 os.munmap(ptr[0 .. buf_aligned_len - new_size_aligned]);
316 return true;
317 }
318
319 // TODO: call mremap
320 // TODO: if the next_mmap_addr_hint is within the remapped range, update it
321 return false;
322 }
323
324 fn free(_: *anyopaque, slice: []u8, log2_buf_align: u8, return_address: usize) void {
325 _ = log2_buf_align;
326 _ = return_address;
327
328 if (builtin.os.tag == .windows) {
329 os.windows.VirtualFree(slice.ptr, 0, os.windows.MEM_RELEASE);
330 } else {
331 const buf_aligned_len = mem.alignForward(slice.len, mem.page_size);
332 const ptr = @alignCast(mem.page_size, slice.ptr);
333 os.munmap(ptr[0..buf_aligned_len]);
334 }
335 }
336};
337
338const WasmPageAllocator = struct {
339 comptime {
340 if (!builtin.target.isWasm()) {
341 @compileError("WasmPageAllocator is only available for wasm32 arch");
342 }
343 }
344
345 const vtable = Allocator.VTable{
346 .alloc = alloc,
347 .resize = resize,
348 .free = free,
349 };
350
351 const PageStatus = enum(u1) {
352 used = 0,
353 free = 1,
354
355 pub const none_free: u8 = 0;
356 };
357
358 const FreeBlock = struct {
359 data: []u128,
360
361 const Io = std.packed_int_array.PackedIntIo(u1, .Little);
362
363 fn totalPages(self: FreeBlock) usize {
364 return self.data.len * 128;
365 }
366
367 fn isInitialized(self: FreeBlock) bool {
368 return self.data.len > 0;
369 }
370
371 fn getBit(self: FreeBlock, idx: usize) PageStatus {
372 const bit_offset = 0;
373 return @intToEnum(PageStatus, Io.get(mem.sliceAsBytes(self.data), idx, bit_offset));
374 }
375
376 fn setBits(self: FreeBlock, start_idx: usize, len: usize, val: PageStatus) void {
377 const bit_offset = 0;
378 var i: usize = 0;
379 while (i < len) : (i += 1) {
380 Io.set(mem.sliceAsBytes(self.data), start_idx + i, bit_offset, @enumToInt(val));
381 }
382 }
383
384 // Use '0xFFFFFFFF' as a _missing_ sentinel
385 // This saves ~50 bytes compared to returning a nullable
386
387 // We can guarantee that conventional memory never gets this big,
388 // and wasm32 would not be able to address this memory (32 GB > usize).
389
390 // Revisit if this is settled: https://github.com/ziglang/zig/issues/3806
391 const not_found = std.math.maxInt(usize);
392
393 fn useRecycled(self: FreeBlock, num_pages: usize, log2_align: u8) usize {
394 @setCold(true);
395 for (self.data) |segment, i| {
396 const spills_into_next = @bitCast(i128, segment) < 0;
397 const has_enough_bits = @popCount(segment) >= num_pages;
398
399 if (!spills_into_next and !has_enough_bits) continue;
400
401 var j: usize = i * 128;
402 while (j < (i + 1) * 128) : (j += 1) {
403 var count: usize = 0;
404 while (j + count < self.totalPages() and self.getBit(j + count) == .free) {
405 count += 1;
406 const addr = j * mem.page_size;
407 if (count >= num_pages and mem.isAlignedLog2(addr, log2_align)) {
408 self.setBits(j, num_pages, .used);
409 return j;
410 }
411 }
412 j += count;
413 }
414 }
415 return not_found;
416 }
417
418 fn recycle(self: FreeBlock, start_idx: usize, len: usize) void {
419 self.setBits(start_idx, len, .free);
420 }
421 };
422
423 var _conventional_data = [_]u128{0} ** 16;
424 // Marking `conventional` as const saves ~40 bytes
425 const conventional = FreeBlock{ .data = &_conventional_data };
426 var extended = FreeBlock{ .data = &[_]u128{} };
427
428 fn extendedOffset() usize {
429 return conventional.totalPages();
430 }
431
432 fn nPages(memsize: usize) usize {
433 return mem.alignForward(memsize, mem.page_size) / mem.page_size;
434 }
435
436 fn alloc(_: *anyopaque, len: usize, log2_align: u8, ra: usize) ?[*]u8 {
437 _ = ra;
438 if (len > maxInt(usize) - (mem.page_size - 1)) return null;
439 const page_count = nPages(len);
440 const page_idx = allocPages(page_count, log2_align) catch return null;
441 return @intToPtr([*]u8, page_idx * mem.page_size);
442 }
443
444 fn allocPages(page_count: usize, log2_align: u8) !usize {
445 {
446 const idx = conventional.useRecycled(page_count, log2_align);
447 if (idx != FreeBlock.not_found) {
448 return idx;
449 }
450 }
451
452 const idx = extended.useRecycled(page_count, log2_align);
453 if (idx != FreeBlock.not_found) {
454 return idx + extendedOffset();
455 }
456
457 const next_page_idx = @wasmMemorySize(0);
458 const next_page_addr = next_page_idx * mem.page_size;
459 const aligned_addr = mem.alignForwardLog2(next_page_addr, log2_align);
460 const drop_page_count = @divExact(aligned_addr - next_page_addr, mem.page_size);
461 const result = @wasmMemoryGrow(0, @intCast(u32, drop_page_count + page_count));
462 if (result <= 0)
463 return error.OutOfMemory;
464 assert(result == next_page_idx);
465 const aligned_page_idx = next_page_idx + drop_page_count;
466 if (drop_page_count > 0) {
467 freePages(next_page_idx, aligned_page_idx);
468 }
469 return @intCast(usize, aligned_page_idx);
470 }
471
472 fn freePages(start: usize, end: usize) void {
473 if (start < extendedOffset()) {
474 conventional.recycle(start, @min(extendedOffset(), end) - start);
475 }
476 if (end > extendedOffset()) {
477 var new_end = end;
478 if (!extended.isInitialized()) {
479 // Steal the last page from the memory currently being recycled
480 // TODO: would it be better if we use the first page instead?
481 new_end -= 1;
482
483 extended.data = @intToPtr([*]u128, new_end * mem.page_size)[0 .. mem.page_size / @sizeOf(u128)];
484 // Since this is the first page being freed and we consume it, assume *nothing* is free.
485 mem.set(u128, extended.data, PageStatus.none_free);
486 }
487 const clamped_start = @max(extendedOffset(), start);
488 extended.recycle(clamped_start - extendedOffset(), new_end - clamped_start);
489 }
490 }
491
492 fn resize(
493 _: *anyopaque,
494 buf: []u8,
495 log2_buf_align: u8,
496 new_len: usize,
497 return_address: usize,
498 ) bool {
499 _ = log2_buf_align;
500 _ = return_address;
501 const aligned_len = mem.alignForward(buf.len, mem.page_size);
502 if (new_len > aligned_len) return false;
503 const current_n = nPages(aligned_len);
504 const new_n = nPages(new_len);
505 if (new_n != current_n) {
506 const base = nPages(@ptrToInt(buf.ptr));
507 freePages(base + new_n, base + current_n);
508 }
509 return true;
510 }
511
512 fn free(
513 _: *anyopaque,
514 buf: []u8,
515 log2_buf_align: u8,
516 return_address: usize,
517 ) void {
518 _ = log2_buf_align;
519 _ = return_address;
520 const aligned_len = mem.alignForward(buf.len, mem.page_size);
521 const current_n = nPages(aligned_len);
522 const base = nPages(@ptrToInt(buf.ptr));
523 freePages(base, base + current_n);
524 }
525};
526
527233pub const HeapAllocator = switch (builtin.os.tag) {
528234 .windows => struct {
529235 heap_handle: ?HeapHandle,
......@@ -1163,7 +869,10 @@ pub fn testAllocatorAlignedShrink(base_allocator: mem.Allocator) !void {
1163869 try testing.expect(slice[60] == 0x34);
1164870}
1165871
1166test "heap" {
1167 _ = @import("heap/logging_allocator.zig");
1168 _ = @import("heap/log_to_writer_allocator.zig");
872test {
873 _ = LoggingAllocator;
874 _ = LogToWriterAllocator;
875 _ = ScopedLoggingAllocator;
876 _ = ArenaAllocator;
877 _ = GeneralPurposeAllocator;
1169878}
lib/std/heap/PageAllocator.zig created+110
......@@ -0,0 +1,110 @@
1const std = @import("../std.zig");
2const builtin = @import("builtin");
3const Allocator = std.mem.Allocator;
4const mem = std.mem;
5const os = std.os;
6const maxInt = std.math.maxInt;
7const assert = std.debug.assert;
8
9pub const vtable = Allocator.VTable{
10 .alloc = alloc,
11 .resize = resize,
12 .free = free,
13};
14
15fn alloc(_: *anyopaque, n: usize, log2_align: u8, ra: usize) ?[*]u8 {
16 _ = ra;
17 _ = log2_align;
18 assert(n > 0);
19 if (n > maxInt(usize) - (mem.page_size - 1)) return null;
20 const aligned_len = mem.alignForward(n, mem.page_size);
21
22 if (builtin.os.tag == .windows) {
23 const w = os.windows;
24 const addr = w.VirtualAlloc(
25 null,
26 aligned_len,
27 w.MEM_COMMIT | w.MEM_RESERVE,
28 w.PAGE_READWRITE,
29 ) catch return null;
30 return @ptrCast([*]align(mem.page_size) u8, @alignCast(mem.page_size, addr));
31 }
32
33 const hint = @atomicLoad(@TypeOf(std.heap.next_mmap_addr_hint), &std.heap.next_mmap_addr_hint, .Unordered);
34 const slice = os.mmap(
35 hint,
36 aligned_len,
37 os.PROT.READ | os.PROT.WRITE,
38 os.MAP.PRIVATE | os.MAP.ANONYMOUS,
39 -1,
40 0,
41 ) catch return null;
42 assert(mem.isAligned(@ptrToInt(slice.ptr), mem.page_size));
43 const new_hint = @alignCast(mem.page_size, slice.ptr + aligned_len);
44 _ = @cmpxchgStrong(@TypeOf(std.heap.next_mmap_addr_hint), &std.heap.next_mmap_addr_hint, hint, new_hint, .Monotonic, .Monotonic);
45 return slice.ptr;
46}
47
48fn resize(
49 _: *anyopaque,
50 buf_unaligned: []u8,
51 log2_buf_align: u8,
52 new_size: usize,
53 return_address: usize,
54) bool {
55 _ = log2_buf_align;
56 _ = return_address;
57 const new_size_aligned = mem.alignForward(new_size, mem.page_size);
58
59 if (builtin.os.tag == .windows) {
60 const w = os.windows;
61 if (new_size <= buf_unaligned.len) {
62 const base_addr = @ptrToInt(buf_unaligned.ptr);
63 const old_addr_end = base_addr + buf_unaligned.len;
64 const new_addr_end = mem.alignForward(base_addr + new_size, mem.page_size);
65 if (old_addr_end > new_addr_end) {
66 // For shrinking that is not releasing, we will only
67 // decommit the pages not needed anymore.
68 w.VirtualFree(
69 @intToPtr(*anyopaque, new_addr_end),
70 old_addr_end - new_addr_end,
71 w.MEM_DECOMMIT,
72 );
73 }
74 return true;
75 }
76 const old_size_aligned = mem.alignForward(buf_unaligned.len, mem.page_size);
77 if (new_size_aligned <= old_size_aligned) {
78 return true;
79 }
80 return false;
81 }
82
83 const buf_aligned_len = mem.alignForward(buf_unaligned.len, mem.page_size);
84 if (new_size_aligned == buf_aligned_len)
85 return true;
86
87 if (new_size_aligned < buf_aligned_len) {
88 const ptr = @alignCast(mem.page_size, buf_unaligned.ptr + new_size_aligned);
89 // TODO: if the next_mmap_addr_hint is within the unmapped range, update it
90 os.munmap(ptr[0 .. buf_aligned_len - new_size_aligned]);
91 return true;
92 }
93
94 // TODO: call mremap
95 // TODO: if the next_mmap_addr_hint is within the remapped range, update it
96 return false;
97}
98
99fn free(_: *anyopaque, slice: []u8, log2_buf_align: u8, return_address: usize) void {
100 _ = log2_buf_align;
101 _ = return_address;
102
103 if (builtin.os.tag == .windows) {
104 os.windows.VirtualFree(slice.ptr, 0, os.windows.MEM_RELEASE);
105 } else {
106 const buf_aligned_len = mem.alignForward(slice.len, mem.page_size);
107 const ptr = @alignCast(mem.page_size, slice.ptr);
108 os.munmap(ptr[0..buf_aligned_len]);
109 }
110}
lib/std/heap/WasmPageAllocator.zig created+193
......@@ -0,0 +1,193 @@
1const std = @import("../std.zig");
2const builtin = @import("builtin");
3const Allocator = std.mem.Allocator;
4const mem = std.mem;
5const maxInt = std.math.maxInt;
6const assert = std.debug.assert;
7
8comptime {
9 if (!builtin.target.isWasm()) {
10 @compileError("WasmPageAllocator is only available for wasm32 arch");
11 }
12}
13
14pub const vtable = Allocator.VTable{
15 .alloc = alloc,
16 .resize = resize,
17 .free = free,
18};
19
20const PageStatus = enum(u1) {
21 used = 0,
22 free = 1,
23
24 pub const none_free: u8 = 0;
25};
26
27const FreeBlock = struct {
28 data: []u128,
29
30 const Io = std.packed_int_array.PackedIntIo(u1, .Little);
31
32 fn totalPages(self: FreeBlock) usize {
33 return self.data.len * 128;
34 }
35
36 fn isInitialized(self: FreeBlock) bool {
37 return self.data.len > 0;
38 }
39
40 fn getBit(self: FreeBlock, idx: usize) PageStatus {
41 const bit_offset = 0;
42 return @intToEnum(PageStatus, Io.get(mem.sliceAsBytes(self.data), idx, bit_offset));
43 }
44
45 fn setBits(self: FreeBlock, start_idx: usize, len: usize, val: PageStatus) void {
46 const bit_offset = 0;
47 var i: usize = 0;
48 while (i < len) : (i += 1) {
49 Io.set(mem.sliceAsBytes(self.data), start_idx + i, bit_offset, @enumToInt(val));
50 }
51 }
52
53 // Use '0xFFFFFFFF' as a _missing_ sentinel
54 // This saves ~50 bytes compared to returning a nullable
55
56 // We can guarantee that conventional memory never gets this big,
57 // and wasm32 would not be able to address this memory (32 GB > usize).
58
59 // Revisit if this is settled: https://github.com/ziglang/zig/issues/3806
60 const not_found = maxInt(usize);
61
62 fn useRecycled(self: FreeBlock, num_pages: usize, log2_align: u8) usize {
63 @setCold(true);
64 for (self.data) |segment, i| {
65 const spills_into_next = @bitCast(i128, segment) < 0;
66 const has_enough_bits = @popCount(segment) >= num_pages;
67
68 if (!spills_into_next and !has_enough_bits) continue;
69
70 var j: usize = i * 128;
71 while (j < (i + 1) * 128) : (j += 1) {
72 var count: usize = 0;
73 while (j + count < self.totalPages() and self.getBit(j + count) == .free) {
74 count += 1;
75 const addr = j * mem.page_size;
76 if (count >= num_pages and mem.isAlignedLog2(addr, log2_align)) {
77 self.setBits(j, num_pages, .used);
78 return j;
79 }
80 }
81 j += count;
82 }
83 }
84 return not_found;
85 }
86
87 fn recycle(self: FreeBlock, start_idx: usize, len: usize) void {
88 self.setBits(start_idx, len, .free);
89 }
90};
91
92var _conventional_data = [_]u128{0} ** 16;
93// Marking `conventional` as const saves ~40 bytes
94const conventional = FreeBlock{ .data = &_conventional_data };
95var extended = FreeBlock{ .data = &[_]u128{} };
96
97fn extendedOffset() usize {
98 return conventional.totalPages();
99}
100
101fn nPages(memsize: usize) usize {
102 return mem.alignForward(memsize, mem.page_size) / mem.page_size;
103}
104
105fn alloc(_: *anyopaque, len: usize, log2_align: u8, ra: usize) ?[*]u8 {
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 @intToPtr([*]u8, 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, @intCast(u32, 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 @intCast(usize, 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 = @intToPtr([*]u128, 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 mem.set(u128, 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 _: *anyopaque,
163 buf: []u8,
164 log2_buf_align: u8,
165 new_len: usize,
166 return_address: usize,
167) bool {
168 _ = log2_buf_align;
169 _ = return_address;
170 const aligned_len = mem.alignForward(buf.len, mem.page_size);
171 if (new_len > aligned_len) return false;
172 const current_n = nPages(aligned_len);
173 const new_n = nPages(new_len);
174 if (new_n != current_n) {
175 const base = nPages(@ptrToInt(buf.ptr));
176 freePages(base + new_n, base + current_n);
177 }
178 return true;
179}
180
181fn free(
182 _: *anyopaque,
183 buf: []u8,
184 log2_buf_align: u8,
185 return_address: usize,
186) void {
187 _ = log2_buf_align;
188 _ = return_address;
189 const aligned_len = mem.alignForward(buf.len, mem.page_size);
190 const current_n = nPages(aligned_len);
191 const base = nPages(@ptrToInt(buf.ptr));
192 freePages(base, base + current_n);
193}