Example: A deserialization function. The process of deserialization itself requires temporary allocations internally. The input data stream is very dynamic and can deserialze into a complex pool of inter-referenced output data.
In the doWork example, the gpa vs arena clearly expresses the usage of the two allocators within the function:
“gpa” indicates that the function requires individual freeing via the allocator, therefore it implies the function is responsible for clearing up anything allocated under which;
“arena” indicates that the function will not use the allocation for individual freeing, therefore it implies that allocations under which will outlive the function under a single lifetime;
Now the term “gpa” and “arena” really are just hints for the caller, I don’t think they are really related to performance. Depending on what is better for the actual logic, the function might allocate a buffer under the gpa to back a FixedBufferAllocator, a Reader, or an ArrayList, or it might even back an internal ArenaAllocator with the gpa.
Again, I think you are looking at this through a very specific viewpoint . You can create an arena from an arena, as far down as you like.
The problem you have described in some sense is solved. If a data structure requires a specific allocator to free it than the deinit function takes an allocator, which is always the same allocator that init takes. If the data structure manages it’s own memory after taking an allocator it stores an allocator as a field and on deinit issues that field to do what it needs to do.
Again to reiterate, you are trying to solve problems through a specific viewpoint, rust. Not everything has to be spelled out semantically, IE THE modeling lifetimes though the semantics of the language, sometimes (always IMO) lifetimes should be handled through the logic of the problem at hand and not ‘as ‘an ‘afterthought ‘tacked ‘onto ‘the ‘semantics ‘of ‘the ‘language.
I feel you want to make it about “the ziggit community has bias vs rust”, but you need to introduce rust because those solution elements are pretty alien to zig and the std. The “interface” is a mean to and end for the dynamic dispatch, not a type system hint about the implementation.
Going back to the brainstorming about the problem of hinting the user about what to use, the current approach of requesting multiple allocator for different types of allocation. What if a GPA was required, but an “arena style allocator” was optional. If not provided, the callee creates an arena or a stack fallback allocator from the non-optional GPA.
If we want the callee to police their caller, I guess the Allocator.VTable could provide hint about the allocator implementation and the callee could assert at runtime that the given allocator respects a contract (e.g. any free vs LIFO free, how much is used for a given len+alignment, how is the locality). Then maybe with a specific build option (e.g. not tests, with the debug allocator), the callee can log warning about bad (performance-wise) allocator choice, and those assert could be the code as documented. Those assert would be a more formal and granular way to convey how the allocator will be used.
Note that the reason why arena: Allocator is passed in is not because the function need an arena internally, but the function result can reference to allocated data, where the caller is responsible of freeing all together.
You have this confusion isn’t because of your skills or the design of Zig…
It’s because many people, including some core team members, including some very “experienced” members on this forum, continually spread some ideas like “passing an Arena (or GPA, or whatever) on BlaBla function is silly”… They are completely BS. The main idea of the Zig’s passing Allocator design is that the users should be able to pass ANY allocator to resuable codes’ function (libraries)… Which allocator is passed should totally depends on the users’ program design, NOT the libraries.
I said again Which allocator is passed should totally depends on the users’ program design, NOT the libraries.
If you are using a library (std or someone’s project), and you are thinking about which allocator should be used, the library is a failure, don’t use them…
If the function is perfectly made, you should think about how your codes are going to use the function…
For example, let’s use the ArrayList as an example… Many people here say that you shouldn’t use arena onto ArrayList… They are 100% wrong… So wrong… For example, you are writing an Archetype ECS system… You have a registry. This registry is most likely based on an ArrayList and a free list… Because your registry will use the free list to handle the remove, instead of order delete and shrink it continually, you can definitely use arena. You worry about growing? You can have a system to check the free list size, when the free list size growing to a certain level, the system will find a less cpu intensive time, or less noticeable time, such as when the user pauses or saves the game, you make a new arena with assumed capacity, and copy the data to it…
Again, which allocator should entirely depend on you, your design, NOT the libraries…
I’m not completely convinced, but simply because I haven’t done enough library dev in zig before. I’ll keep the idea in mind to see if it can be justified in the general case.
But anyways, whether it is good or bad library design, it is still pretty common out there for APIs to expect an allocator capable of arbitrary individual freeing.
Then don’t use those “APIs”… They are not even considered reusable…
If they expect you to have an Allocator with a certain capability, why don’t they just init the allocator themselves in the function? Oh, just because this is not Zig way… Then why Zig way then?
I am paying people to develop programs on a bare metal microcontroller in Zig… The controller has no heap.
You still like to use some ArrayList liked functions. Sure, then which allocator are you going to use? You probably will go with fixed buffer allocator… And you can’t even free anything literally…
Then only way to shrink the ArrayList is to make two buffers, once one growing to a certain level, you copy the ArrayList to another buffer, and reset the old one… Next time, you do the reverse copy, reset… People think it’s silly, but it’s blasting fast… And you literally make your own heap…
FWIW my solution to this is often to return something akin to a struct { foo: *Bar, arena: std.heap.ArenaAllocator }. This very clearly indicates to the caller that they’re being given back memory that is “leaky”, but has leaked into the arena, and can be freed all at once.
You can also simply name your function fn doWorkLeaky(gpa: std.mem.Allocator, ... ), and that indicates to the caller to not pass a regular allocator.
this requires creating a new arena per call, whereas the caller might benefit from reusing the same arena, or potentially FBA / StackFallbackAllocator / others.
There’s also one obvious path to highest performance; avoiding using an allocator as much as possible.
For instance, 1993 DOOM allocates 8 MB of memory on program startup, and then manages this block of memory internally.
While this approach obviously means that your program might sometimes reserve more memory than it actually uses, the advantage is that the program never once needs to do a syscall to reserve memory after startup.
Another pattern I’ve talked about in the past is writing functions to accept non-const slices and/or pointers instead of an allocator, so that the user has full control over exactly when and how the memory is allocated and/or freed.
This allows the user to pass slices that they actually haven’t allocated manually, such as pub var buffers.
It also allows them to pass small slices of large allocations, making it much easier to batch-allocate/free objects.
Of course, there’s also an obvious reason not to program this way, which feeds into what you said - it’s a very heavy-handed, “C” approach to memory management.
Newer languages have stuff like object lifetimes for a reason, and that’s because it makes it trivially easy to manage memory correctly.
Zig’s way of doing that is:
I am surprised that you consider passing an Arena a signal that the caller should manage the returned objects. In my mind what makes Arena different from Gpa is that it has a reset method, so if I see a method that requires an Arena allocator I expect it is so because it wants to call reset on it, meaning it wants an Arena for its own temporary allocations that it will free itself.
Instead of it being passed the concrete ArenaAllocator, it is moreso being passed an abstract Allocator named “arena”.
When you consider arena allocation to be a pattern where you can do individual allocation but can only free all together, the fact that the function is given only the allocator interface of the arena, I think it’s enough to imply that whoever owns the actual arena instance (i.e. the caller) is responsible of the free.
I think it would be cool if the Allocator interface was extended, to provide the ability to just get an Arena or an ephemeral allocator in general, that would have it’s own freelist and like you said be reused. Like you would have your normal GPA, but inside there would be a gpa.arena(), which gives you a std.heap.AreneaAllocator. and makes it so you can just reuse the same arena over and over across calls. that be very neat, the alternative that I find works great, is to give each type which needs temporary allocation it’s own arena, but it’s indeed a bit wasteful especially if the memory usage could theoretically be overlapped with each type was using the same arena sequentially.
While all the bikeshedding is fun and good for fleshing out potential considerations, in the end there’s no substitute for writing the code to explore the idea. Zig’s allocators are all implemented in user space and your alternative design can be also. Nothing like writing code against your design to discover if it’s any good.