#Need help understanding memory allocators for HashMaps.

1 messages · Page 1 of 1 (latest)

tawny glade
#

I wanted to play around with HashMaps and used the example from the Zig documentation (pasted below). The example works great, but taking the example and putting it in a main function does not work. I have to pass in an allocator that works outside of testing.

I have tried a number of things, like reading a bit about allocators and trying to pass in different allocators, but I cannot figure out how to make it work.

My questions:

  • can someone give me a working example where a HashMap is created in a main function with a memory allocator?
  • what are good resources on better understanding memory allocators? I am used to garbage collected languages and find that the existing explanations on the topic in the docs don't really make me understand how to work with them
test "hashing" {
    const Point = struct { x: i32, y: i32 };

    var map = std.AutoHashMap(u32, Point).init(
        test_allocator,
    );
    defer map.deinit();

    try map.put(1525, .{ .x = 1, .y = -4 });
    try map.put(1550, .{ .x = 2, .y = -3 });
    try map.put(1575, .{ .x = 3, .y = -2 });
    try map.put(1600, .{ .x = 4, .y = -1 });

    try expect(map.count() == 4);

    var sum = Point{ .x = 0, .y = 0 };
    var iterator = map.iterator();

    while (iterator.next()) |entry| {
        sum.x += entry.value_ptr.x;
        sum.y += entry.value_ptr.y;
    }

    try expect(sum.x == 10);
    try expect(sum.y == -10);
}
next sentinel
#

in zig, std.mem.Allocator is an interface over memory allocation. As an interface, it doesn't provide any way in particular to instantiate or deinitialize it - this responsibility is left to the implementer. But two very common patterns are:

  1. Using global singletons, such as std.testing.allocator, or std.heap.page_allocator - generally these aren't meant to be used as-is or in production, but rather as the base for other allocators, although that's not a hard and fast rule (e.g. std.heap.c_allocator, with the caveat that in using it you end up depending on libc).

  2. Initialising some local state that the interface points to:

var impl: AllocatorImpl = ...;
defer impl.deinit();
const allocator: std.mem.Allocator = impl.allocator();

done usually in your entry point, such that you pass down the allocator to any functions that need it, either because those functions will use it to allocate, or they call functions that will.

As a concrete example, you could change the top of your test to something like this, and you'd be able to move it into your main function:

var gpa: std.heap.GeneralPurposeAllocator(.{}) = .{};
defer _ = gpa.deinit(); // <- checks for leaks at the end of scope
const allocator = gpa.allocator();

var map = std.AutoHashMap(u32, Point).init(allocator);
defer map.deinit();
#

do note that std.heap.GeneralPurposeAllocator is not very performant at present, and if you need it to be, or if you're benchmarking things, you may want to reach for std.heap.c_allocator, or any other appropriate implementation