const std = @import("std"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; const Alignment = std.mem.Alignment; const Slab = struct { alignment: Alignment, item_size: usize, max_slot_count: usize, bitset: std.bit_set.DynamicBitSetUnmanaged, chunks: std.ArrayListUnmanaged([]u8), pub fn init( allocator: Allocator, alignment: Alignment, item_size: usize, max_slot_count: usize, ) !Slab { return .{ .alignment = alignment, .item_size = item_size, .bitset = try .initFull(allocator, 0), .chunks = .empty, .max_slot_count = max_slot_count, }; } pub fn deinit(self: *Slab, allocator: Allocator) void { self.bitset.deinit(allocator); for (self.chunks.items) |chunk| { allocator.rawFree(chunk, self.alignment, @returnAddress()); } self.chunks.deinit(allocator); } inline fn calculateChunkSize(self: *Slab, chunk_index: usize) usize { const safe_index: u6 = @intCast(@min(std.math.maxInt(u6), chunk_index)); const exponential = @as(usize, 1) << safe_index; return @min(exponential, self.max_slot_count); } inline fn toBitsetIndex(self: *Slab, chunk_index: usize, slot_index: usize) usize { var offset: usize = 0; for (0..chunk_index) |i| { const chunk_size = self.calculateChunkSize(i); offset += chunk_size; } return offset + slot_index; } inline fn toChunkAndSlotIndices(self: *Slab, bitset_index: usize) struct { usize, usize } { var offset: usize = 0; var chunk_index: usize = 0; while (chunk_index < self.chunks.items.len) : (chunk_index += 1) { const chunk_size = self.calculateChunkSize(chunk_index); if (bitset_index < offset + chunk_size) { return .{ chunk_index, bitset_index - offset }; } offset += chunk_size; } unreachable; } fn alloc(self: *Slab, allocator: Allocator) ![]u8 { if (self.bitset.findFirstSet()) |index| { const chunk_index, const slot_index = self.toChunkAndSlotIndices(index); // if we have a free slot self.bitset.unset(index); const chunk = self.chunks.items[chunk_index]; const offset = slot_index * self.item_size; return chunk.ptr[offset..][0..self.item_size]; } else { const old_capacity = self.bitset.bit_length; // if we have don't have a free slot try self.allocateChunk(allocator); const first_slot_index = old_capacity; self.bitset.unset(first_slot_index); const new_chunk = self.chunks.items[self.chunks.items.len - 1]; return new_chunk.ptr[0..self.item_size]; } } fn free(self: *Slab, ptr: [*]u8) void { const addr = @intFromPtr(ptr); for (self.chunks.items, 0..) |chunk, i| { const chunk_start = @intFromPtr(chunk.ptr); const chunk_end = chunk_start + chunk.len; if (addr >= chunk_start and addr < chunk_end) { const offset = addr - chunk_start; const slot_index = offset / self.item_size; const bitset_index = self.toBitsetIndex(i, slot_index); assert(!self.bitset.isSet(bitset_index)); self.bitset.set(bitset_index); return; } } unreachable; } fn allocateChunk(self: *Slab, allocator: Allocator) !void { const next_chunk_size = self.calculateChunkSize(self.chunks.items.len); const chunk_len = self.item_size * next_chunk_size; const chunk_ptr = allocator.rawAlloc( chunk_len, self.alignment, @returnAddress(), ) orelse return error.FailedChildAllocation; const chunk = chunk_ptr[0..chunk_len]; try self.chunks.append(allocator, chunk); const new_capacity = self.bitset.bit_length + next_chunk_size; try self.bitset.resize(allocator, new_capacity, true); } const Stats = struct { key: SlabKey, item_size: usize, chunk_count: usize, total_slots: usize, slots_in_use: usize, slots_free: usize, bytes_allocated: usize, bytes_in_use: usize, bytes_free: usize, utilization_ratio: f64, }; fn getStats(self: *const Slab, key: SlabKey) Stats { const total_slots = self.bitset.bit_length; const free_slots = self.bitset.count(); const used_slots = total_slots - free_slots; const bytes_allocated = total_slots * self.item_size; const bytes_in_use = used_slots * self.item_size; const utilization_ratio = if (bytes_allocated > 0) @as(f64, @floatFromInt(bytes_in_use)) / @as(f64, @floatFromInt(bytes_allocated)) else 0.0; return .{ .key = key, .item_size = self.item_size, .chunk_count = self.chunks.items.len, .total_slots = total_slots, .slots_in_use = used_slots, .slots_free = free_slots, .bytes_allocated = bytes_allocated, .bytes_in_use = bytes_in_use, .bytes_free = free_slots * self.item_size, .utilization_ratio = utilization_ratio, }; } }; const SlabKey = struct { size: usize, alignment: Alignment, }; pub const SlabAllocator = struct { const Self = @This(); child_allocator: Allocator, max_slot_count: usize, slabs: std.ArrayHashMapUnmanaged(SlabKey, Slab, struct { const Context = @This(); pub fn hash(_: Context, key: SlabKey) u32 { var hasher = std.hash.Wyhash.init(0); std.hash.autoHash(&hasher, key.size); std.hash.autoHash(&hasher, key.alignment); return @truncate(hasher.final()); } pub fn eql(_: Context, a: SlabKey, b: SlabKey, _: usize) bool { return a.size == b.size and a.alignment == b.alignment; } }, false) = .empty, pub fn init(child_allocator: Allocator, max_slot_count: usize) Self { assert(std.math.isPowerOfTwo(max_slot_count)); return .{ .child_allocator = child_allocator, .slabs = .empty, .max_slot_count = max_slot_count, }; } pub fn deinit(self: *Self) void { for (self.slabs.values()) |*slab| { slab.deinit(self.child_allocator); } self.slabs.deinit(self.child_allocator); } pub const ResetKind = enum { /// Free all chunks and release all memory. clear, /// Keep all chunks, reset trees to reuse memory. retain_capacity, }; /// This clears all of the stored memory, freeing the currently used chunks. pub fn reset(self: *Self, kind: ResetKind) void { switch (kind) { .clear => { for (self.slabs.values()) |*slab| { for (slab.chunks.items) |chunk| { self.child_allocator.free(chunk); } slab.chunks.clearAndFree(self.child_allocator); slab.bitset.deinit(self.child_allocator); } self.slabs.clearAndFree(self.child_allocator); }, .retain_capacity => { for (self.slabs.values()) |*slab| { slab.bitset.setAll(); } }, } } const Stats = struct { total_allocated_bytes: usize, bytes_in_use: usize, bytes_free: usize, slab_count: usize, total_chunks: usize, total_slots: usize, slots_in_use: usize, slots_free: usize, fragmentation_ratio: f64, utilization_ratio: f64, slabs: []const Slab.Stats, pub fn print(self: *const Stats, stream: *std.io.Writer) !void { try stream.print("\n", .{}); try stream.print("\n=== Slab Allocator Statistics ===\n", .{}); try stream.print("Overall Memory:\n", .{}); try stream.print(" Total allocated: {} bytes ({d:.2} MB)\n", .{ self.total_allocated_bytes, @as(f64, @floatFromInt(self.total_allocated_bytes)) / 1_048_576.0, }); try stream.print(" In use: {} bytes ({d:.2} MB)\n", .{ self.bytes_in_use, @as(f64, @floatFromInt(self.bytes_in_use)) / 1_048_576.0, }); try stream.print(" Free: {} bytes ({d:.2} MB)\n", .{ self.bytes_free, @as(f64, @floatFromInt(self.bytes_free)) / 1_048_576.0, }); try stream.print("\nOverall Structure:\n", .{}); try stream.print(" Slab Count: {}\n", .{self.slab_count}); try stream.print(" Total chunks: {}\n", .{self.total_chunks}); try stream.print(" Total slots: {}\n", .{self.total_slots}); try stream.print(" Slots in use: {}\n", .{self.slots_in_use}); try stream.print(" Slots free: {}\n", .{self.slots_free}); try stream.print("\nOverall Efficiency:\n", .{}); try stream.print(" Utilization: {d:.1}%\n", .{self.utilization_ratio * 100.0}); try stream.print(" Fragmentation: {d:.1}%\n", .{self.fragmentation_ratio * 100.0}); if (self.slabs.len > 0) { try stream.print("\nPer-Slab Breakdown:\n", .{}); try stream.print( " {s:>5} | {s:>4} | {s:>6} | {s:>6} | {s:>6} | {s:>10} | {s:>6}\n", .{ "Size", "Algn", "Chunks", "Slots", "InUse", "Bytes", "Util%" }, ); try stream.print( " {s:-<5}-+-{s:-<4}-+-{s:-<6}-+-{s:-<6}-+-{s:-<6}-+-{s:-<10}-+-{s:-<6}\n", .{ "", "", "", "", "", "", "" }, ); for (self.slabs) |slab| { try stream.print(" {d:5} | {d:4} | {d:6} | {d:6} | {d:6} | {d:10} | {d:5.1}%\n", .{ slab.key.size, @intFromEnum(slab.key.alignment), slab.chunk_count, slab.total_slots, slab.slots_in_use, slab.bytes_allocated, slab.utilization_ratio * 100.0, }); } } } }; pub fn getStats(self: *Self, a: std.mem.Allocator) !Stats { var slab_stats: std.ArrayList(Slab.Stats) = try .initCapacity(a, self.slabs.entries.len); errdefer slab_stats.deinit(a); var stats = Stats{ .total_allocated_bytes = 0, .bytes_in_use = 0, .bytes_free = 0, .slab_count = self.slabs.count(), .total_chunks = 0, .total_slots = 0, .slots_in_use = 0, .slots_free = 0, .fragmentation_ratio = 0.0, .utilization_ratio = 0.0, .slabs = &.{}, }; var it = self.slabs.iterator(); while (it.next()) |entry| { const key = entry.key_ptr.*; const slab = entry.value_ptr; const slab_stat = slab.getStats(key); slab_stats.appendAssumeCapacity(slab_stat); stats.total_allocated_bytes += slab_stat.bytes_allocated; stats.bytes_in_use += slab_stat.bytes_in_use; stats.bytes_free += slab_stat.bytes_free; stats.total_chunks += slab_stat.chunk_count; stats.total_slots += slab_stat.total_slots; stats.slots_in_use += slab_stat.slots_in_use; stats.slots_free += slab_stat.slots_free; } if (stats.total_allocated_bytes > 0) { stats.fragmentation_ratio = @as(f64, @floatFromInt(stats.bytes_free)) / @as(f64, @floatFromInt(stats.total_allocated_bytes)); stats.utilization_ratio = @as(f64, @floatFromInt(stats.bytes_in_use)) / @as(f64, @floatFromInt(stats.total_allocated_bytes)); } stats.slabs = try slab_stats.toOwnedSlice(a); return stats; } pub const vtable = Allocator.VTable{ .alloc = alloc, .free = free, .remap = Allocator.noRemap, .resize = Allocator.noResize, }; pub fn allocator(self: *Self) Allocator { return .{ .ptr = self, .vtable = &vtable, }; } fn alloc(ctx: *anyopaque, len: usize, alignment: Alignment, ret_addr: usize) ?[*]u8 { const self: *Self = @ptrCast(@alignCast(ctx)); _ = ret_addr; const aligned_len = std.mem.alignForward(usize, len, alignment.toByteUnits()); const list_gop = self.slabs.getOrPut( self.child_allocator, SlabKey{ .size = aligned_len, .alignment = alignment }, ) catch return null; if (!list_gop.found_existing) { list_gop.value_ptr.* = Slab.init( self.child_allocator, alignment, aligned_len, self.max_slot_count, ) catch return null; } const list = list_gop.value_ptr; const buf = list.alloc(self.child_allocator) catch return null; return buf[0..len].ptr; } fn free(ctx: *anyopaque, memory: []u8, alignment: Alignment, ret_addr: usize) void { const self: *Self = @ptrCast(@alignCast(ctx)); _ = ret_addr; const ptr = memory.ptr; const len = memory.len; const aligned_len = std.mem.alignForward(usize, len, alignment.toByteUnits()); const list = self.slabs.getPtr(.{ .size = aligned_len, .alignment = alignment }).?; list.free(ptr); } }; const testing = std.testing; const TestSlabAllocator = SlabAllocator; test "slab allocator - basic allocation and free" { var slab_alloc = TestSlabAllocator.init(testing.allocator, 16); defer slab_alloc.deinit(); const allocator = slab_alloc.allocator(); // Allocate some memory const ptr1 = try allocator.alloc(u8, 100); try testing.expect(ptr1.len == 100); // Write to it to ensure it's valid @memset(ptr1, 42); try testing.expectEqual(@as(u8, 42), ptr1[50]); // Free it allocator.free(ptr1); } test "slab allocator - multiple allocations" { var slab_alloc = TestSlabAllocator.init(testing.allocator, 16); defer slab_alloc.deinit(); const allocator = slab_alloc.allocator(); const ptr1 = try allocator.alloc(u8, 64); const ptr2 = try allocator.alloc(u8, 128); const ptr3 = try allocator.alloc(u8, 256); // Ensure they don't overlap const addr1 = @intFromPtr(ptr1.ptr); const addr2 = @intFromPtr(ptr2.ptr); const addr3 = @intFromPtr(ptr3.ptr); try testing.expect(addr1 + 64 <= addr2 or addr2 + 128 <= addr1); try testing.expect(addr2 + 128 <= addr3 or addr3 + 256 <= addr2); allocator.free(ptr1); allocator.free(ptr2); allocator.free(ptr3); } test "slab allocator - no coalescing (different size classes)" { var slab_alloc = TestSlabAllocator.init(testing.allocator, 16); defer slab_alloc.deinit(); const allocator = slab_alloc.allocator(); // Allocate two blocks of same size const ptr1 = try allocator.alloc(u8, 128); const ptr2 = try allocator.alloc(u8, 128); // Free them (no coalescing in slab allocator) allocator.free(ptr1); allocator.free(ptr2); // Can't allocate larger block from these freed 128-byte blocks const ptr3 = try allocator.alloc(u8, 256); // ptr3 will be from a different size class, not coalesced from ptr1+ptr2 const addr1 = @intFromPtr(ptr1.ptr); const addr3 = @intFromPtr(ptr3.ptr); // They should NOT be adjacent (different size classes) try testing.expect(addr3 < addr1 or addr3 >= addr1 + 256); allocator.free(ptr3); } test "slab allocator - reuse freed memory" { var slab_alloc = TestSlabAllocator.init(testing.allocator, 16); defer slab_alloc.deinit(); const allocator = slab_alloc.allocator(); const ptr1 = try allocator.alloc(u8, 64); const addr1 = @intFromPtr(ptr1.ptr); allocator.free(ptr1); // Allocate same size, should reuse from same slab const ptr2 = try allocator.alloc(u8, 64); const addr2 = @intFromPtr(ptr2.ptr); try testing.expectEqual(addr1, addr2); allocator.free(ptr2); } test "slab allocator - multiple size classes" { var slab_alloc = TestSlabAllocator.init(testing.allocator, 16); defer slab_alloc.deinit(); const allocator = slab_alloc.allocator(); // Allocate various sizes - each creates a new slab var ptrs: [10][]u8 = undefined; const sizes = [_]usize{ 24, 40, 64, 88, 128, 144, 200, 256, 512, 1000 }; for (&ptrs, sizes) |*ptr, size| { ptr.* = try allocator.alloc(u8, size); @memset(ptr.*, 0xFF); } // Should have created multiple slabs try testing.expect(slab_alloc.slabs.count() >= 10); // Free all for (ptrs) |ptr| { allocator.free(ptr); } } test "slab allocator - various sizes" { var slab_alloc = TestSlabAllocator.init(testing.allocator, 16); defer slab_alloc.deinit(); const allocator = slab_alloc.allocator(); // Test different sizes (not limited to powers of 2!) const sizes = [_]usize{ 8, 16, 24, 32, 40, 64, 88, 128, 144, 256 }; for (sizes) |size| { const ptr = try allocator.alloc(u8, size); try testing.expect(ptr.len == size); @memset(ptr, @intCast(size & 0xFF)); allocator.free(ptr); } } test "slab allocator - exact sizes (no rounding)" { var slab_alloc = TestSlabAllocator.init(testing.allocator, 16); defer slab_alloc.deinit(); const allocator = slab_alloc.allocator(); // Odd sizes stay exact (unlike buddy which rounds to power of 2) const ptr1 = try allocator.alloc(u8, 100); const ptr2 = try allocator.alloc(u8, 200); const ptr3 = try allocator.alloc(u8, 50); // Exact sizes! try testing.expect(ptr1.len == 100); try testing.expect(ptr2.len == 200); try testing.expect(ptr3.len == 50); allocator.free(ptr1); allocator.free(ptr2); allocator.free(ptr3); } test "slab allocator - chunk allocation" { var slab_alloc = TestSlabAllocator.init(testing.allocator, 16); defer slab_alloc.deinit(); const allocator = slab_alloc.allocator(); // Allocate many items of same size to force multiple chunks var ptrs: [100][]u8 = undefined; for (&ptrs) |*ptr| { ptr.* = try allocator.alloc(u8, 64); } // Should have allocated multiple chunks (32 items per chunk) const slab = slab_alloc.slabs.getPtr(.{ .size = 64, .alignment = Alignment.@"1" }).?; try testing.expect(slab.chunks.items.len > 1); // Free all for (ptrs) |ptr| { allocator.free(ptr); } } test "slab allocator - reset with retain_capacity" { var slab_alloc = TestSlabAllocator.init(testing.allocator, 16); defer slab_alloc.deinit(); const allocator = slab_alloc.allocator(); // Allocate some memory const ptr1 = try allocator.alloc(u8, 128); const ptr2 = try allocator.alloc(u8, 256); _ = ptr1; _ = ptr2; const slabs_before = slab_alloc.slabs.count(); const slab_128 = slab_alloc.slabs.getPtr(.{ .size = 128, .alignment = Alignment.@"1" }).?; const chunks_before = slab_128.chunks.items.len; // Reset but keep chunks slab_alloc.reset(.retain_capacity); try testing.expectEqual(slabs_before, slab_alloc.slabs.count()); try testing.expectEqual(chunks_before, slab_128.chunks.items.len); // Should be able to allocate again const ptr3 = try allocator.alloc(u8, 512); allocator.free(ptr3); } test "slab allocator - reset with clear" { var slab_alloc = TestSlabAllocator.init(testing.allocator, 16); defer slab_alloc.deinit(); const allocator = slab_alloc.allocator(); // Allocate some memory const ptr1 = try allocator.alloc(u8, 128); _ = ptr1; try testing.expect(slab_alloc.slabs.count() > 0); // Reset and free everything slab_alloc.reset(.clear); try testing.expectEqual(@as(usize, 0), slab_alloc.slabs.count()); // Should still work after reset const ptr2 = try allocator.alloc(u8, 256); allocator.free(ptr2); } test "slab allocator - stress test" { var slab_alloc = TestSlabAllocator.init(testing.allocator, 16); defer slab_alloc.deinit(); const allocator = slab_alloc.allocator(); var prng = std.Random.DefaultPrng.init(0); const random = prng.random(); var ptrs: std.ArrayList([]u8) = .empty; defer { for (ptrs.items) |ptr| { allocator.free(ptr); } ptrs.deinit(allocator); } // Random allocations and frees var i: usize = 0; while (i < 100) : (i += 1) { if (random.boolean() and ptrs.items.len > 0) { // Free a random allocation const index = random.uintLessThan(usize, ptrs.items.len); allocator.free(ptrs.swapRemove(index)); } else { // Allocate random size (8 to 512) const size = random.uintAtMost(usize, 504) + 8; const ptr = try allocator.alloc(u8, size); try ptrs.append(allocator, ptr); // Write to ensure it's valid @memset(ptr, @intCast(i & 0xFF)); } } } test "slab allocator - alignment" { var slab_alloc = TestSlabAllocator.init(testing.allocator, 16); defer slab_alloc.deinit(); const allocator = slab_alloc.allocator(); const ptr1 = try allocator.create(u64); const ptr2 = try allocator.create(u32); const ptr3 = try allocator.create([100]u8); allocator.destroy(ptr1); allocator.destroy(ptr2); allocator.destroy(ptr3); } test "slab allocator - no resize support" { var slab_alloc = TestSlabAllocator.init(testing.allocator, 16); defer slab_alloc.deinit(); const allocator = slab_alloc.allocator(); const slice = try allocator.alloc(u8, 100); @memset(slice, 42); // Resize should fail (not supported) try testing.expect(!allocator.resize(slice, 90)); try testing.expect(!allocator.resize(slice, 200)); allocator.free(slice); } test "slab allocator - fragmentation pattern" { var slab_alloc = TestSlabAllocator.init(testing.allocator, 16); defer slab_alloc.deinit(); const allocator = slab_alloc.allocator(); // Allocate 10 items var items: [10][]u8 = undefined; for (&items) |*item| { item.* = try allocator.alloc(u8, 64); @memset(item.*, 0xFF); } // Free every other one allocator.free(items[0]); allocator.free(items[2]); allocator.free(items[4]); allocator.free(items[6]); allocator.free(items[8]); // Allocate new items - should reuse freed slots const new1 = try allocator.alloc(u8, 64); const new2 = try allocator.alloc(u8, 64); const new3 = try allocator.alloc(u8, 64); // Should get some of the freed slots back const addrs = [_]usize{ @intFromPtr(items[0].ptr), @intFromPtr(items[2].ptr), @intFromPtr(items[4].ptr), @intFromPtr(items[6].ptr), @intFromPtr(items[8].ptr), }; const new1_addr = @intFromPtr(new1.ptr); var found = false; for (addrs) |addr| { if (new1_addr == addr) found = true; } try testing.expect(found); // Cleanup allocator.free(items[1]); allocator.free(items[3]); allocator.free(items[5]); allocator.free(items[7]); allocator.free(items[9]); allocator.free(new1); allocator.free(new2); allocator.free(new3); } test "slab allocator - many small allocations" { var slab_alloc = TestSlabAllocator.init(testing.allocator, 16); defer slab_alloc.deinit(); const allocator = slab_alloc.allocator(); // Allocate 1000 small items var ptrs: std.ArrayList([]u8) = .empty; defer { for (ptrs.items) |ptr| { allocator.free(ptr); } ptrs.deinit(allocator); } var i: usize = 0; while (i < 1000) : (i += 1) { const ptr = try allocator.alloc(u8, 24); try ptrs.append(allocator, ptr); } // Should have created multiple chunks const slab = slab_alloc.slabs.getPtr(.{ .size = 24, .alignment = Alignment.@"1" }).?; try testing.expect(slab.chunks.items.len > 1); } test "slab allocator - zero waste for exact sizes" { var slab_alloc = TestSlabAllocator.init(testing.allocator, 16); defer slab_alloc.deinit(); const allocator = slab_alloc.allocator(); // These sizes have zero internal fragmentation (unlike buddy) const sizes = [_]usize{ 24, 40, 56, 88, 144, 152, 184, 232, 648 }; for (sizes) |size| { const ptr = try allocator.alloc(u8, size); // Exact size returned! try testing.expectEqual(size, ptr.len); @memset(ptr, 0xFF); allocator.free(ptr); } } test "slab allocator - different size classes don't interfere" { var slab_alloc = TestSlabAllocator.init(testing.allocator, 16); defer slab_alloc.deinit(); const allocator = slab_alloc.allocator(); // Allocate size 64 const ptr_64 = try allocator.alloc(u8, 64); const addr_64 = @intFromPtr(ptr_64.ptr); allocator.free(ptr_64); // Allocate size 128 - should NOT reuse size-64 slot const ptr_128 = try allocator.alloc(u8, 128); const addr_128 = @intFromPtr(ptr_128.ptr); try testing.expect(addr_64 != addr_128); // Allocate size 64 again - SHOULD reuse original slot const ptr_64_again = try allocator.alloc(u8, 64); const addr_64_again = @intFromPtr(ptr_64_again.ptr); try testing.expectEqual(addr_64, addr_64_again); allocator.free(ptr_128); allocator.free(ptr_64_again); } test "slab allocator - 16-byte alignment" { var slab_alloc = TestSlabAllocator.init(testing.allocator, 16); defer slab_alloc.deinit(); const allocator = slab_alloc.allocator(); // Request 16-byte aligned memory const ptr = try allocator.alignedAlloc(u8, .@"16", 152); defer allocator.free(ptr); // Verify alignment const addr = @intFromPtr(ptr.ptr); try testing.expect(addr % 16 == 0); // Make sure we can use it @memset(ptr, 0xFF); } test "slab allocator - various alignments" { var slab_alloc = TestSlabAllocator.init(testing.allocator, 16); defer slab_alloc.deinit(); const allocator = slab_alloc.allocator(); const alignments = [_]std.mem.Alignment{ .@"1", .@"2", .@"4", .@"8", .@"16" }; inline for (alignments) |alignment| { const ptr = try allocator.alignedAlloc(u8, alignment, 100); defer allocator.free(ptr); const addr = @intFromPtr(ptr.ptr); const align_value = alignment.toByteUnits(); try testing.expect(addr % align_value == 0); } }