Why does everyone love being able to control the allocator in Zig? I rarely want this low level control over the allocator, I would rather have an easy to use interface to malloc/free (such as RAII) or would prefer the language push me to avoiding allocations when unnecessary. C and Zig users are always talking about how arena allocators are easier to use than the alternative but I disagree because the alternative is extremely easy to use when the language makes it so.

Arenas have a lot of footguns like growing out of control if you don't have space in your logic to delete them (e.g. suppose every new HTTP connection required obtaining a new allocation from the primary arena -- now you have a memory leak). They also aren't necessarily faster than malloc: I remember hearing a case where mimalloc was faster than an arena for a one-shot program like a linker because reclaiming space improved cache locality. Plus it's difficult to implement things like growable vectors or hashmaps using an arena without wasting space.

I just don't see how "futz about with the allocator constantly" and "use an arena" is actually a good default.

It is a good default for lib code because it makes that code truly re-usable. The same is true for great C libraries, they let you provide a block of memory instead of calling malloc inside. The library should not get to decide if/when allocations should be done.

In Zig app code you don't have to pass allocators all over if you don't want to, that's your code so do what you want. But for libraries it's the right thing to do because it puts the caller in control.

The reason you have to pass allocators instead of making this implicit like in e.g. Odin, is because Zig just takes a firm stance against implicit logic, firmer than any other language I've ever used. The end result is that people who like Zig tend to like passing allocators around even in their own code.

The library developer doesn't know what allocator will be passed, and the library user doesn't necessarily know how it will be used. They share an interface, so it will [probably] work, but it might not behave (mem/perf wise) as either expects. ArenaAllocators in particular don't play great with ArrayLists or nested ArenaAllocators.

I don't think it's a huge problem, but it's more subtle than an explicit allocator is a better because it provides more flexibility.

> Zig just takes a firm stance against implicit logic

I bought that as Zig's M.O., but then came the automatic "async/IO" colorless functions which is a huge chunk of implicit and hidden logic.

That's more implicit logic than what's in say JavaScript async AFAICT. Arbitrary int sizes with implicit casting rules, etc. More recently was the attempts of memory safety via an some sort of (optional) implicit GC mode. Personally I don't get it.

Are you sure you're up to date?

The async design changed multiple times, and the current approach is that of requiring passing an I/O interface that encapsulates the logic, same approach as for passing around an allocator.

This effectively makes all I/O bound code "quasi-async", and it's all explicit and the caller is in control.

As far as I understand it, the idea is that the IO interface is "magic" in the sense that it affects which version of a function gets called: either the synchronous, linear version, or the asynchronous, stackless, suspensible state machine version.

This has a lot of implications, especially for function pointers and other forms of dynamic dispatch.

My personal take is that async code is fundamentally different from sync code, because the weights of various tradeoffs change dramatically. In my view, "colorless" functions are not really a worthwhile goal in the first place. But I've been wrong before, so it may turn out to be great, we'll see.

There is no magic about the I/O interface. It's a regular dynamic dispatch, the same as if you use virtual functions in C++. Currently, and in the foreseeable future, there are only two options how it can run, blocking mode where each task occupies a dedicated thread, or using stackful coroutines. The interface implements blocking functions, there is no async waiting anywhere in there. But because it's dynamic dispatch, it allows the async implementation to do the "prepare operation, suspend until completed, wake up" dance, where the threaded implementation would just run the blocking syscall directly. There are plans to add stackless coroutines, but that will require many language restrictions to happen first, so it's not going to happen any time soon and when it does, the interface will likely change to accomodate it.

If you don't like arenas then that will push you towards avoiding allocations :)

I don't love malloc / free, managing frees one-to-one with allocations is a nightmare. An HTTP server is actually a great example of a good fit for arenas... you can have an arena created per connection, and then free the arena when the connection closes. Now all of your allocations during that connection are guaranteed to be freed instead of having to keep track of them individually. Need something to live across connections? Use the general purpose allocator for those particular things.

For one shot programs, why bother freeing anything at all? The gpa or malloc or whatever else is fine there.

Personally I mostly lean on fixed-size buffers, but if I'm writing Zig it's just little toy programs where blowing up when the buffer runs out of room is fine. But for big non-toy programs it might be good to use fixed-size buffers anyway and define your limits in terms of things actually relevant to your domain.

RAII and smart pointers are fine too, but C++ has you covered if you want those. Zig is specifically avoiding that kind of invisible code that destructors bring you.

> Now all of your allocations during that connection are guaranteed to be freed instead of having to keep track of them individually

And how do you size the arena?

> keep track of them individually

I think this is an exaggeration of the problem. This is what RAII semantics in C++ are for.

> avoiding that kind of invisible code

The invisible code is traded for defer statements right after initialization that are obvious and therefore mostly just noise, and also a problem if you forget to call them. This isn't an issue with RAII. I get the point of not wanting invisible code, but defers don't seem much of an improvement either. A trade-off at best.

> And how do you size the arena?

Two options: you either say "eh 20MB per request ought to be big enough" and resize it later if you need to. Or you take time to figure out reasonable limits in terms of whatever domain you're dealing with, and return descriptive errors when those limits are exceeded. Maybe you're saying your service supports up to N HTTP headers and M query parameters in the URL and so on. For almost anything there's a limit you can place that's reasonable. Having no limits at all just means the limit is when you run out of memory / disk / whatever.

If you like RAII and C++ just use that though, Zig is an alternative not an upgrade, imo.

Can arenas be made to use no actual memory unless it is being used, so the virtual memory subsystem only allocates pages when written to?

> And how do you size the arena?

All HTTP servers have a configurable maximum request size. Ex: client_max_body_size in Nginx.

> This is what RAII semantics in C++ are for.

...which typically has more overhead than a good GC though and much more overhead than using arenas for lifetime management.

> If you don't like arenas then that will push you towards avoiding allocations

I mean push in the sense of a pit-of-success, not making me do things because the alternative is unpalatable.

> managing frees one-to-one with allocations is a nightmare

It really isn't a nightmare if your language has tools to help you. I'm never particularly concerned about frees in Rust outside of thinking about room to optimize.

> Need something to live across connections? Use the general purpose allocator for those particular things.

Well, this is a determination you have to make very carefully and if you screw it up (like in the situation I was trying to outline) you have a subtle bug.

> Personally I mostly lean on fixed-size buffers, but if I'm writing Zig it's just little toy programs where blowing up when the buffer runs out of room is fine.

What's also fine is using malloc, which will have perfectly reasonable performance for your toy programs.

This is reinforcing my belief that Zig programmers actually just have a sort of aesthetic attachment to the idea of managing their allocations themselves and this has very little to do with the actual practical concerns of writing software that works. I'm a lot more sympathetic to the other reply that talked about library code improving flexibility for those relying on the library, than this comment that only thinks about the case where everything goes perfectly and using a fixed size buffer for no reason.

> I'm never particularly concerned about frees in Rust outside of thinking about room to optimize.

Zig's position, love it or hate it, is that you should be thinking about it. Dropping a Vec<String> in Rust is O(n). Have you ever clicked the X on an app and it takes 10 seconds to disappear? Stuff like this is part of the problem. You shouldn't have to traverse your process's whole memory space just to call exit(0).

If you have to write that for loop, the O(n) will jump out at you and push you towards an arena or FixedBufferAllocator or std.process.cleanExit, or any other solution to avoid the loop.

> Stuff like this is part of the problem.

Measure, measure, measure.

So often when people insist that a thing they don't like is "part of the problem" they haven't even thought about how much that "part" would be, although I suppose it's worse when they have thought and decided to go ahead anyway because the weasel wording might convince the hard of thinking.

A guy who rounded up from $14.86 to $15 on some grant paperwork and got 14 cents he wasn't entitled to out of a government programme, and Ken Paxton inexplicably becoming very wealthy from a government job are both "part of the problem" but hopefully it's apparent which of these is worth fixing.

Just to let you know, `std::process::exit(...)` in Rust does not call any destructors. C++ also has `std::quick_exit(...)`. Applications exiting slowly is a problem caused by app developers.

(There are cases where it also takes time because the OS does have to flush all open files during process teardown, etc., but this shouldn't generally be user-visible.)

Dropping vecs that contain strings does not make any Rust code in the real world slow. You would need to have a vec containing billions of strings, occupying several gigs of memory, before dropping such a structure took any perceptible amount of time. And for extreme cases like that, you can simply use arenas. The bumpalo crate is approximately as old as Zig is. The reason that people so rarely turn to arenas in Rust isn't because you can't do them, because you've always been able to, but rather because arenas are a very specific solution to a specific sort of memory access pattern, and going out of your way to strictly adhere to that pattern doesn't produce measurable differences for most software.

> This is reinforcing my belief that Zig programmers actually just have a sort of aesthetic attachment to the idea of managing their allocations themselves and this has very little to do with the actual practical concerns of writing software that works.

Indeed if you don't want to manage everything yourself I don't think there's a need to use Zig. The whole pitch is making everything explicit and not hiding control flow. And I do think that limits its usefulness to smaller-scale codebases.

Writing quality software doesn't require much from the programming language until your codebase gets very large and you're collaborating with a ton of people. If I'm expecting a codebase to grow to a large scale I'm not reaching for Zig for that. I don't trust _myself_ to keep that much Zig code tidy and correct let alone a big mob of developers and agents :)

My opinion is quite the opposite.

With a small program, it's rarely difficult to work out what's going on. If your small program develops a performance problem, it's usually fairly tractable to work out what's going on, and modify it. In extremis, you can just rewrite the whole thing.

Large programs are another matter. Hidden control flows in C++ (for example) can make pinning down performance problems extremely difficult. And then widespread refactoring can be extremely challenging.

Zig forces everything to be an explicit interface, which should make maintenance work on large programs much more tractable.

if you don't want to, you can just pick the general purpose allocator and use it everywhere.

however, in languages that don't allow it, you can't pick and choose different allocators for different workflows. it may not be important to you, but it is important to many. and shoe horning support for multiple allocators is not easy for language

Passing allocator pointers around has the advantage of allowing you to customize the allocator when you want, and to ignore it when you want (which is most of the time), i.e. just use the allocator you were given and pass it around like some piece of syntax or ctx in Go.

The trouble comes when you have data structures that manage memory, and you want to associate the allocation strategy with the particular instance of the data structure. Or, worse, you don't store the allocator pointer with the data and you require that all functions operating on the data structure happen to use compatible allocators.

At its simplest, though, passing allocators around is just removing an implicit global variable that is sometimes helpful to customize locally, but nearly impossible to customize locally if left as a global.

That just means you haven't run into the problems yet that a global allocator brings with it. Other people have and welcome the better control over allocation provided by the Zig std library. The challenge is of course to balance convenience and flexibility. Also if you write code that prefers to not allocate in the first place (which should be the default), you're not affected anyway.

Allocators help solve the problem of "this code I want to use is almost good enough; how do I actually use it?"

I distinctly remember one of my first "real" jobs, somebody had written a C# library for GraphQL. It had some sort of bug or lack of functionality I had to fix. The author had fallen hook, line, and sinker for sealed classes and all that nonsense, so the shortest path to using the rest of their code was to fork the library and re-write the thousand offending lines and all references thereto. Luckily it wasn't a deep dependency.

Contrast that with similar issues in typical Python. The library likely accepts my favourite duck-typed solution, if it doesn't then I can likely work around it with an appropriate choice of dunder methods in the code I have interacting with the library, and if it still doesn't yield then I can just rewrite (monkey-patch) the offending code at runtime.

Not that either of those scenarios are objectively optimal, but they illustrate one of the principles people have in mind when they use an interface like allocators (not the only one, I'm sure other commentors will talk your ear off), that when push comes to shove you'd prefer to be able to use your existing code to solve your new problems. And that does empirically mostly work here; you can use nearly the entire Zig stdlib (especially when ignoring things like the posix namespace) and most of the rest of the ecosystem as well on embedded devices, in WASM, in bounded-memory applications, in generic application code, in fast-running CLI apps not even bothering to free, etc.

I agree with much of your discussion about arenas by the way. They're a moderately poor fit for the current interface in Zig, largely because the free functionality is questionable at best. IME, most uses of arenas actually know they need such a thing. Functions taking allocator arguments and expecting the allocator to be an arena should take an arena argument. When using arenas for lifetime management, something should probably own that arena rather than implicitly assuming whatever gets passed through the stack is correct. Passing arenas to arbitrary functions expecting general-purpose allocators is prone to leaks, and you'd be better off using the arena (or a buffer created therefrom) as a backing allocator for a data structure capable of actually freeing if you can't trust the implementation you're calling into (and if you can trust it, they should expose a better API allowing you to detect that trustability at the callsite).

I also don't get it, programming since 1986, in quite a good number of programming languages, including various kinds of systems programming alternatives.

To me this kind of stuff always feels a bit cargo cult programming.

If you had to pick a default, an arena allocator would definitely be it. Although even better would be a fixed-size arena that can never grow, like Tigerbeetle does.

I don't think the footguns you're talking about are real by the way.

arena's are popular in part because they are a way of grouping allocations by a single lifetime, reducing the amount of use-after-free's or double-free's that occur (reducing, not eliminating them).

they make a lot less sense in languages where the compiler or runtime takes care of this for you.

The biggest interesting thing to me about Zig is that is is a true spiritual successor to C.

mimalloc aggressively reclaims space and it benefits the use case in our Dip database built on rust.

This is for a long running process where consuming compute and memory should only occur on processing a request and must revert to a baseline memory consumption once the request is completed.

malloc with default allocator always gradually increases the memory consumption. This continues with enough request bursts on our testing until the process runs out of memory. mimalloc doesn't.

On mobile, we are stuck with the default allocator. On mobiles, the phenomenon of the default allocator consuming memory is reproducible even with normal Qt apps. It is what it is.

> This continues with enough request bursts on our testing until the process runs out of memory

Curious, how much memory and how many requests does that take to exhaust?

You are looking at this from the wrong perspective. If you are fine with automatic allocations and RAII like you have in C++ or Rust, or with a garbage collected language, there is pretty much no reason to even look at Zig.

Zig is a language to use when you do need the control, mostly because of performance reasons. And once you are in, you start thinking how to manage your allocations and arenas are the logical outcome, once you notice that your application has scopes that can be destroyed at once.

If you need an argument why arenas are better than RAII, then you likely don't have the performance needs to even consider using Zig, because outside of performance, arenas are not better.