Skip to content

Commit bbb9e90

Browse files
committed
Add: NUMA-aware allocator for Zig
1 parent 86b95aa commit bbb9e90

File tree

2 files changed

+166
-4
lines changed

2 files changed

+166
-4
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,5 @@
3939
Cargo.lock
4040
target/
4141
.zig-cache/
42-
zig-out/
42+
zig-out/
43+
zig-cache/

zig/fork_union.zig

Lines changed: 164 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,137 @@ pub fn allocate(numa_node_index: usize, bytes: usize) ?[*]u8 {
222222
return @ptrCast(@alignCast(ptr));
223223
}
224224

225+
/// NUMA-aware allocator compatible with Zig's allocator interface.
226+
pub const NumaAllocator = struct {
227+
node_index: usize,
228+
229+
const Self = @This();
230+
const Allocator = std.mem.Allocator;
231+
232+
const Header = packed struct {
233+
base_addr: usize,
234+
allocated_bytes: usize,
235+
};
236+
237+
const vtable = Allocator.VTable{
238+
.alloc = alloc,
239+
.resize = resize,
240+
.remap = remap,
241+
.free = free,
242+
};
243+
244+
pub fn init(node_index: usize) Self {
245+
return .{ .node_index = node_index };
246+
}
247+
248+
pub fn allocator(self: *Self) Allocator {
249+
return .{ .ptr = self, .vtable = &vtable };
250+
}
251+
252+
fn alloc(ctx: *anyopaque, len: usize, alignment: std.mem.Alignment, ret_addr: usize) ?[*]u8 {
253+
_ = ret_addr;
254+
const self: *Self = @ptrCast(@alignCast(ctx));
255+
const effective_len = if (len == 0) 1 else len;
256+
const slice = self.allocSlice(effective_len, alignment) orelse return null;
257+
return slice.ptr;
258+
}
259+
260+
fn resize(
261+
ctx: *anyopaque,
262+
buf: []u8,
263+
alignment: std.mem.Alignment,
264+
new_len: usize,
265+
ret_addr: usize,
266+
) bool {
267+
_ = ret_addr;
268+
const self: *Self = @ptrCast(@alignCast(ctx));
269+
return self.resizeInPlace(buf, alignment, new_len);
270+
}
271+
272+
fn remap(
273+
ctx: *anyopaque,
274+
buf: []u8,
275+
alignment: std.mem.Alignment,
276+
new_len: usize,
277+
ret_addr: usize,
278+
) ?[*]u8 {
279+
_ = ret_addr;
280+
const self: *Self = @ptrCast(@alignCast(ctx));
281+
const result = self.remapSlice(buf, alignment, new_len) orelse return null;
282+
return result.ptr;
283+
}
284+
285+
fn free(ctx: *anyopaque, buf: []u8, alignment: std.mem.Alignment, ret_addr: usize) void {
286+
_ = alignment;
287+
_ = ret_addr;
288+
const self: *Self = @ptrCast(@alignCast(ctx));
289+
self.freeSlice(buf);
290+
}
291+
292+
fn allocSlice(self: *Self, len: usize, alignment: std.mem.Alignment) ?[]u8 {
293+
const header_size = @sizeOf(Header);
294+
const alignment_bytes = alignment.toByteUnits();
295+
const with_header = std.math.add(usize, len, header_size) catch return null;
296+
const request_bytes = std.math.add(usize, with_header, alignment_bytes) catch return null;
297+
298+
var allocated_bytes: usize = undefined;
299+
var bytes_per_page: usize = undefined;
300+
const raw_ptr = c.fu_allocate_at_least(
301+
self.node_index,
302+
request_bytes,
303+
&allocated_bytes,
304+
&bytes_per_page,
305+
) orelse return null;
306+
307+
const base_addr = @intFromPtr(raw_ptr);
308+
const data_addr = alignment.forward(base_addr + header_size);
309+
if (data_addr + len > base_addr + allocated_bytes) {
310+
c.fu_free(self.node_index, raw_ptr, allocated_bytes);
311+
return null;
312+
}
313+
314+
const header_ptr = @as(*Header, @ptrFromInt(data_addr - header_size));
315+
header_ptr.* = .{
316+
.base_addr = base_addr,
317+
.allocated_bytes = allocated_bytes,
318+
};
319+
320+
const data_ptr = @as([*]u8, @ptrFromInt(data_addr));
321+
return data_ptr[0..len];
322+
}
323+
324+
fn resizeInPlace(self: *Self, buf: []u8, alignment: std.mem.Alignment, new_len: usize) bool {
325+
_ = self;
326+
_ = alignment;
327+
if (buf.len == 0) return false;
328+
if (new_len == 0) return false;
329+
if (new_len <= buf.len) return true;
330+
return false;
331+
}
332+
333+
fn remapSlice(self: *Self, buf: []u8, alignment: std.mem.Alignment, new_len: usize) ?[]u8 {
334+
if (buf.len == 0) return null;
335+
if (new_len == 0) {
336+
self.freeSlice(buf);
337+
return buf[0..0];
338+
}
339+
if (new_len <= buf.len) return buf[0..new_len];
340+
341+
const new_slice = self.allocSlice(new_len, alignment) orelse return null;
342+
@memcpy(new_slice[0..buf.len], buf);
343+
self.freeSlice(buf);
344+
return new_slice;
345+
}
346+
347+
fn freeSlice(self: *Self, buf: []u8) void {
348+
if (buf.len == 0) return;
349+
const header_ptr = @as(*Header, @ptrFromInt(@intFromPtr(buf.ptr) - @sizeOf(Header)));
350+
const header = header_ptr.*;
351+
const base_ptr = @as(*anyopaque, @ptrFromInt(header.base_addr));
352+
c.fu_free(self.node_index, base_ptr, header.allocated_bytes);
353+
}
354+
};
355+
225356
/// Thread pool for fork-join parallelism
226357
pub const Pool = struct {
227358
handle: *anyopaque,
@@ -367,7 +498,7 @@ pub const Pool = struct {
367498
func(prong, typed_ctx.*);
368499
}
369500
};
370-
c.fu_pool_for_n(self.handle, n, Wrapper.callback, @constCast(@ptrCast(&context)));
501+
c.fu_pool_for_n(self.handle, n, Wrapper.callback, @ptrCast(@constCast(&context)));
371502
}
372503
}
373504

@@ -431,7 +562,7 @@ pub const Pool = struct {
431562
func(prong, typed_ctx.*);
432563
}
433564
};
434-
c.fu_pool_for_n_dynamic(self.handle, n, Wrapper.callback, @constCast(@ptrCast(&context)));
565+
c.fu_pool_for_n_dynamic(self.handle, n, Wrapper.callback, @ptrCast(@constCast(&context)));
435566
}
436567
}
437568

@@ -499,7 +630,7 @@ pub const Pool = struct {
499630
func(prong, count, typed_ctx.*);
500631
}
501632
};
502-
c.fu_pool_for_slices(self.handle, n, Wrapper.callback, @constCast(@ptrCast(&context)));
633+
c.fu_pool_for_slices(self.handle, n, Wrapper.callback, @ptrCast(@constCast(&context)));
503634
}
504635
}
505636

@@ -691,3 +822,33 @@ test "NUMA allocation" {
691822
slice[i] = @intCast(i & 0xFF);
692823
}
693824
}
825+
826+
test "NUMA allocator integrates with std collections" {
827+
std.debug.print("Running test: NUMA allocator integrates with std collections\n", .{});
828+
if (!numaEnabled()) return error.SkipZigTest;
829+
830+
var numa_alloc = NumaAllocator.init(0);
831+
const allocator = numa_alloc.allocator();
832+
833+
var list = try std.ArrayList(u64).initCapacity(allocator, 0);
834+
defer list.deinit(allocator);
835+
try list.appendSlice(allocator, &[_]u64{ 1, 2, 3, 4, 5 });
836+
try std.testing.expectEqual(@as(usize, 5), list.items.len);
837+
try std.testing.expectEqual(@as(u64, 3), list.items[2]);
838+
839+
var map = std.AutoHashMap(u32, u32).init(allocator);
840+
defer map.deinit();
841+
try map.put(10, 100);
842+
try map.put(20, 200);
843+
try map.put(30, 300);
844+
try std.testing.expectEqual(@as(usize, 3), map.count());
845+
try std.testing.expectEqual(@as(u32, 200), map.get(20).?);
846+
847+
var buf = try allocator.alloc(u8, 128);
848+
defer allocator.free(buf);
849+
@memset(buf, 0xAB);
850+
851+
buf = try allocator.realloc(buf, 512);
852+
try std.testing.expectEqual(@as(usize, 512), buf.len);
853+
try std.testing.expectEqual(@as(u8, 0xAB), buf[0]);
854+
}

0 commit comments

Comments
 (0)