Hacker News
points by philippta 2 days ago | 0 comments
On the Odin vs Zig allocator bit:

In Zig you pass an allocator to every function that allocates, even if it doesn't make a lot of sense.

    const arr = std.ArrayList(i64).empty;
    arr.append(allocator, 1);
    arr.append(allocator, 2);
    arr.append(allocator, 3);
This rarely makes sense, as it's not really useful to append using a different allocator than you used previously.

In Odin, allocators are passed in the init, stored as part of the struct and reused in the append.

    arr := make([dynamic]i64, 0, 10, context.allocator)
    append(&arr, 1)
    append(&arr, 2)
    append(&arr, 3)
[–] kristoff_it 1 day ago
We used to have a version of ArrayList that bundled an allocator in the stdlib, we removed it because our experience having written Zig code for a few years led us to reach for the allocator-less variant almost always.

One aspect your argument is missing is that if you are juggling many ArrayLists, having each bundle its own copy of the allocator (instead of storing it just once) means wasting 16 bytes per ArrayList. The allocator-less variant is simply more flexible in that regard.

[–] dpatterbee 1 day ago
ArrayLists used to store an allocator at init time and then use it for appends and whatnot afterwards. It was decided that this hid allocations and wasn't appropriate for the standard library.

It is of course trivial to define your own arraylist type that wraps an allocator on initialisation if you so wish.

[–] tialaramex 1 day ago
Odin ends up pointlessly carrying around Allocator structures for each such container, each of which is two words (so e.g. 16 bytes on a modern system) because of how Odin works.

So e.g. that growable array of i64 you've asked for - just the data structure itself, not the array which was the whole point - needs 40 bytes on a modern machine because it has 8 bytes for a pointer to the array, 8 bytes for the size of that array, 8 bytes for how much was used, 8 bytes for a pointer to the allocator procedures and 8 bytes for a (sometimes unused) metadata structure for that allocator.

It's not a huge amount of waste, but what you get for it isn't worth it.

In Zig they're at least not paying for this with runtime space, they're paying for it with the programmer's need to make sure they talk about the same allocator everywhere.

[–] rallyroger 1 day ago
But instead you bloat up every call site with an extra argument that needs to be passed, which takes up a few bytes of instructions + claiming a register for it which could have been used for other arguments (and having the allocator in a register is probably a waste since it's a hopefully rare thing to need to access).
[–] bunderbunder 1 day ago
I wonder how bad that really is in practice.

Hypothetically the code is consolidating sites where allocation happens. Zig’s design seems to really push you toward this by making anything else a PITA. So then that allocator reference is being used frequently and already sitting in a register anyway.

The few extra instructions for explicitly passing it might be small compared to the cost of having objects carry their own references. That would grow each instance by ~8 bytes. It would mean that the pointer to the allocator needs to be pulled from a unique memory location for each instance, which is maybe an order of magnitude slower than already having it in a register. Larger data structures would also mean more cache misses, which are another order of magnitude slower.

And this is all chicken feed compared to actually taking a walk of shame out to the DRAM to fetch more memory. So the language ergonomics encouraging you to consolidate your allocation might trump all other considerations in practice.

[–] tialaramex 1 day ago
Note that in Odin you are already paying for an extra register because you silently move a magic "context" variable around in a register.

I haven't looked at the details in Odin, but it's definitely sensible to have the growth decision inline, which means only the (rare) growth itself needs the extra parameter.

[–] philippta 1 day ago
FWIW you can make procedures contextless:

    proc "contextless" ()
[–] tialaramex 1 day ago
You can, but Odin does not mark these procedures contextless and indeed it mustn't because in some cases it will fill out the allocator values by looking in the context, which it couldn't do if it did not have context.
[–] 14113 1 day ago
But you need to keep track of the allocator somewhere, as otherwise you don't know how to deallocate a container, or its contents. You either pay a very small cost in memory, or have to structure all your code to keep track of the allocator for everything you create.
[–] tialaramex 1 day ago
Or it can just live in the type system, as it does in Rust. This is basically always going to be cheaper, and for the case where you just want a single global default allocator it's free instead of two pointers.

I think with Rust 1.100 next month you could write an OdinStyleAllocator type which impl std::alloc::Allocator and has the two pointers inside it, and do the thing Odin does here if you wanted that but I expect you'd snap out of it.

[–] winrid 1 day ago
That's because in Zig that would be seen as a surprise side effect - the allocator is expected when the action is performed, not when it's "prepared", this way you know append() allocates. Otherwise it wouldn't be explicit which Array methods allocated.

The Odin way is useful, too. Depends on what you're building.

[–] brabel 1 day ago
The whole argument that you always know when Zig allocates is just wrong, as this shows. They “fixed” the hidden allocation by making it explicit in the append function but now you can pass in the wrong allocator. Not great is it? It is just all so fragile and relying on convention rather than hard checks. People sometimes pursue a bad idea to incredible lengths and then everything else is justifiable based on that initial, bad idea that just causes much more pain than it solves.
[–] winrid 1 day ago
What do you mean wrong allocator? What if you want to use a different one (will it work?)
[–] philippta 1 day ago
If the array list has enough capacity, you don't know if it allocated or not.
[–] leecommamichael 1 day ago
We’re talking about libraries here. I prefer Odin’s library for my own uses because it ends up making lots of things tidy. In either language you can have it your way.
[–] jstimpfle 1 day ago
That's just a matter of wrapping the allocator if you feel fhat is better approach. Question is, should there always be an allocator around with arr or do you want to split a build phase from an immutable use phase.