I wonder why Zig didn't decide on the convention of making the allocator type comptime-known. It would make code faster by eliminating a runtime indirect call, and it would let you lower memory usage if you choose a zero-sized type as your allocator, since e.g. every single ArrayList in your app would no longer need to reserve space for the allocator field.
I believe that the language designers did some experimentation and designed the vtable struct so that calls to it can often be devirtualized. See this blog article for more details https://pithlessly.github.io/allocgate.html
This is correct, my understanding is devirtualization is quite sufficient in general and fine for Zig's allocator interface. The challenge/solution outlined in my article should not be read as "vtable bad!" but rather "vtable bad..if it means you lose ABI compatibility and have to copy struct data for ABI compatibility everywhere in the end"
If devirtualization does reliably occur then that's probably good enough. I find myself surprised that they would rely on compiler magic for that, it seems to go against the ethos of the language. But in the grand scheme of things, I guess it's a pretty minor cost.
Having the allocator type be part of the collection type means every thing that needs to work with that collection also needs to be generic on the allocator type. It's just busywork.
(Rust has made the decision to have the A: Allocator be part of the collection type, and it's already causing the problem where all existing code only works with A = GlobalAlloc)
You don't need to use ArrayList if you know which allocator you want to use with it from some other means. You can use ArrayListUnmanaged. An ArrayList is equivalent to an ArrayListUnmanaged and Allocator.
Zig already encourages you to pass the allocator around as a value. Passing it as a type would be about the same amount of typing. Instead of this:
var list = ArrayList(u8).init(my_allocator);
you do this:
var list = ArrayList(u8, MyAllocator).init();
How is that more busywork?
Rust's issue is not really related. They added the generic after 1.0 and so it needed to have a default for backwards compatibility. If it had been generic from the start with a matching lint (like `implicit_hasher`) things would be fine. Zig on the other hand is not yet 1.0, so they can afford to make allocgate-like changes.
>Zig already encourages you to pass the allocator around as a value.
That has nothing to do with what I said. I said the Allocator is not part of the type. An ArrayList(u8) is the same as another ArrayList(u8) regardless of what allocator they use.
>How is that more busywork?
Write a function that operates on any ArrayList(u8). It doesn't need to be generic itself. If the Allocator was part of the type it would also need to be generic. And its caller would need to be generic, and so on.
>Rust's issue is not really related. They added the generic after 1.0 and so it needed to have a default for backwards compatibility.
False. std::io::Read::read_to_end takes a Vec<u8>. It cannot be changed to take a Vec<u8, A> because that would require read_to_end to be generic on A, which cannot be done because that would make std::io::Read no longer be object-safe.
And to be clear, the point is not that std::io::Read can't change without being backward-compatible. The point is about needing to be generic on the allocator causes all sorts of problems, eg the infectious genericity (which also affects Zig) and the lack of object-safe-ness.
> And its caller would need to be generic, and so on.
My point though, was that it's extremely similar to code you already write. Zig encourages you to write `fn foo(allocator: Allocator, ...)`. That would just become something like `fn foo(allocator_type: anytype, ...)`. Sure one is generic and one isn't, but since types are just normal parameters, it works out to the same amount of typing.
> std::io::Read::read_to_end takes a Vec<u8>
In Rust, read_to_end is part of a trait and appears in a vtable, so it can't be generic. The Zig equivalent `Reader.readAllArrayList` however, is a free function that is statically resolved. Object safety isn't a concern. In fact, Reader already has generic functions, such as `readInt`. `readAllArrayList` could be generic for the same reason that `readInt` can be.
>My point though, was that it's extremely similar to code you already write. Zig encourages you to write `fn foo(allocator: Allocator, ...)`.
No function that takes an ArrayList(u8) needs to have an Allocator parameter.
>In Rust, read_to_end is part of a trait and appears in a vtable, so it can't be generic.
That's what my comment already said, yes.
>Object safety isn't a concern.
??? It was an example of the downside of Rust's choice. Object safety is a concern for std::io::Read::read_to_end. Rust's choice of having the Allocator be part of the type has forever doomed std::io::Read::read_to_end users from being unable to use any allocator for the Vec other than GlobalAlloc.
I'm not sure why you keep steering the conversation to Rust. It doesn't matter how doomed Rust's Read trait is. Zig is a totally different language. Zig is not bound by the same design tradeoffs. That is why it's possible for Zig to have generic functions in its Reader interface, such as today's `readInt`, or tomorrow's `readAllArrayList`.
Mate, I'm not "steering" the conversation to Rust. The paragraph you responded to was about Rust in the first place.
I clearly said there are two downsides of having the Allocator be part of the type. One, the busywork from having to propagate the type parameter, applies to Zig and Rust. The other, the fact that generic methods can't go in vtables, only applies to Rust.
How do you implement arena allocators in that situation? Or allocate from a fixed-size buffer?
{
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit(); // All allocations made by the above allocator will be freed at the end of scope
const allocator = arena.allocator();
var i = 0;
while (i < 1000000) : (i += 1) {
_ = try allocator.create(i32);
}
// all allocations are freed
}
var buffer: [1000]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buffer);
const allocator = fba.allocator();
// the allocator will return an address inside of `buffer`
_ = try allocator.create([512]u8);
> Having the allocator type be part of the collection type means every thing that needs to work with that collection also needs to be generic on the allocator type.
Maybe I'm misunderstanding but... Not necessarily? since zig is duck-typed at comptime, you could in principle pass the allocator in as a parameter "at runtime" and let the compiler figure out the allocator type.
I wonder why Zig didn't decide on the convention of making the allocator type comptime-known. It would make code faster by eliminating a runtime indirect call, and it would let you lower memory usage if you choose a zero-sized type as your allocator, since e.g. every single ArrayList in your app would no longer need to reserve space for the allocator field.