Comment by awesan
8 days ago
Zig (like C) is simply not a good language to use if you're going to do many small allocations with uncorrelated lifetimes. To write robust Zig (or C) code, you must manage lifetimes yourself, for example by grouping allocations on an arena or by having fixed buffers of "things".
You can just do that, and then Zig is really no less robust than Rust. But if you want to do "managed language" style allocation patterns (like what llms generally prefer), it doesn't make sense to use it.
Grouped and arena allocations work really well in Zig. For awhile, we tried to use this pattern almost everywhere in Bun but it gets really tricky when there’s some GC-managed memory and you want to free things incrementally to reduce RSS. Also, using arenas for arrays that grow wastes memory a lot since it keeps every previous version around (mimalloc arenas are slightly better for this)
Grouped allocations works especially well in parsers & ASTs where the lifetime is very bounded. Since the Rust rewrite, we still use arenas for Bun’s parsers and the bundler but not a ton elsewhere.
> using arenas for arrays that grow wastes memory a lot since it keeps every previous version around
Not if you use segmented arrays
https://danielchasehooper.com/posts/segment_array/
> You can just do that, and then Zig is really no less robust than Rust.
If you just don't write bugs, then yes all languages are equally robust, including assembly.
Zig, like C, is simply not a robust language. I don't know why this feels like something contentious? It's clearly not intended to be robust?
Zig is intended to be as robust as it can be as long as it doesn’t implicitly add code (no destructors that run code you didn’t explicitly call), or increase compiler complexity and compilation time.
I don’t think it makes sense to say Zig is or isn’t intended to be robust in general. Like, we don’t say Rust isn’t robust since it doesn’t add dependent types and general purpose static verification that can do more general proofs. It’s focused on eliminating one class of memory bugs in particular, exactly the class of bugs that are the biggest challenge for software like Bun, and other software with complex lifetimes (it originated from Mozilla and Rust is perfect for browsers)
Zig is intended to be robust for software like TigerBeetle, or the Zig compiler itself, where memory lifetimes are simple.
I’d say the focus on built in tests, fuzzing, debug memory allocators and safe mode shows that Zig is absolutely intended to be robust, within the scope of what the language aims to be. Far more than C itself or most of its popular compilers ever did.
A casual read of TigerBeetle's practices makes it clear they're doing some very unusual things, both in their memory allocation strategy and in their testing/verification.
Despite TigerBeetle being one of the highest-profile remaining Zig projects, I actually don't think they're representative of the average Zig project at all.
15 replies →
> Like, we don’t say Rust isn’t robust since it doesn’t add dependent types and general purpose static verification that can do more general proofs.
Give it a few years! I've noticed an explosion in interest in formal verification recently, especially since nowadays the bar to entry is so low: just ask your LLM agent to give it a go.
Realistically much of the most reliable software in the world was written in C. Robustness is more so a function of coding style and engineering practice than it is of the programming language chosen.
Of course you could argue that on average, most programmers are not going to have the right practices and skill, so on average you should prefer Rust. But that's unrelated to the argument I was making, and in any case not a very interesting point in my opinion.
Adding that with a principled approach, I don't even see much of an issue with doing manual creates and deletes in a object-graph type app, with many unstructured lifetimes. Sometimes that might just be required, and then the complexity is just there either way. Having to cleanups manually or not doesn't change anything about that. It's a bit more cumbersome to get everything right when doing it manually -- sure.
The problem is mostly people graduating from school thinking that somehow there is only stack and heap, and malloc/free is how you do heap. That view completely ignores that the essence of programming systems is mostly to understand the machine, and then doing conceptual and architectural work on a solution (and also on a problem). The act of writing actual code is then mostly just translating those concepts into the digital world verbatim.
Conversely, most of the high impact bugs are also written in C and C++, because they rely on "coding style and engineering practice" to be correct. Rust raises the floor on this by a lot.
Above poster is talking about thinking in terms of grouped lifetimes and bulk allocations/deallocations, which is better for performance, and makes Rust borrow checking and other RAII style features pointless as they don't add any safety benefits. This video completely changed the way I think, and I subsequently moved on from Rust: https://www.youtube.com/watch?v=xt1KNDmOYqA
the buffer managemnt is just different pattern and style of code thats more low level. when you care about performance and cpu cache, you have to make sure that actual physical memory gets computed at same time as other memory near it so there is less latency.
the primary motivation isn't latency but complexity. People do in some applications free or allocate collectively because they have interrupt times in mind, but most of the time when you manually manage memory the issue is mental overhead, so people gravitate towards models they can keep in their head.
Allocating in large chunks is often not very performant which is why people came up with tools like the borrow checker, you often want to allocate and deallocate dynamically on a need-basis but that's exactly where bugs occur.
1 reply →
> Zig, like C, is simply not a robust language
"Extraordinary claims require extraordinary evidence" -- Carl Sagan
> You can just do that, and then Zig is really no less robust than Rust.
That's just it, using Zig required more rigorous engineering than the Bun team were capable of.
People tried to "just do that" (write their programs in a memory safe way) in C and other languages with manual memory management for decades, and we have countless vulnerabilities that prove this simply doesn't work. That's why newer languages, with the notable exception of Zig, prefer more advanced memory management methods.
> People tried to "just do that" (write their programs in a memory safe way) in C and other languages with manual memory management for decades, and we have countless vulnerabilities that prove this simply doesn't work.
Who are these "people" you speak of? It's possible to write software in low level languages that don't have these problems. Not a "non-zero" it might be possible, it can be done thoughtfully, and the popular notion it can't be done is backed only by incomplete anecdotes.
Should everything be written in low-level languages? No, that would be absurd. Is it a simple fact of life that not every person/team/organisation is capable of meeting certain standards of rigour? Yes. That's not to say anyone in the Bun team could not become sufficiently competent in the future. For whatever reason, current experience, incentives, and personal motivations did not make for a conducive environment to make Bun watertight in Zig.
3 replies →
That's like saying that they should have used assembler but they just weren't capable of it. It's personal insult dressed up as a nonsensical technical argument.
Similar in style to Zig's guy public insulting
It matters what software you're writing. When it's a JavaScript runtime, it's not as if you can get away with preallocating all the memory you'll need like some games used to do.
Is engineering capability the issue, or time pressure/constraint?
So it's reducible to a simple matter of inferiority vs superiority?
I think the main issue is that Bun relies heavily on existing C++ libraries like JavascriptCore, and these require RAII and ref-counting semantics from C++ that are closer to Rust than Zig.
You could write a JS engine with Zig-like idioms (arena allocation, static initialization), but that would require re-writing the whole JS engine from the ground-up (though I would definitely be interested in it if someone actually tries to do it!)
> You could write a JS engine with Zig-like idioms (arena allocation, static initialization)
Arena allocators & static initializers are not novel. You'll find them in high performance C++ projects as well, such as LLVM or JavaScriptCore. But arena allocators have the quite significant limitation that they only help when everything being allocated in them have approximately the same lifetime. So they don't help when you need to allocate memory to provide the native implementation of a JavaScript object, for example (eg, FFI).
> You could write a JS engine with Zig-like idioms (arena allocation, static initialization)
Could you actually? That seems like a bad fit for a JS engine to me. Predictable memory requirements are great when you can have them, perhaps you can avoid complexity then, but for a JS engine?
Why not using Swift?
Swift is very weak outside Apple ecosystem, compared to Rust. Not sure nowadays, but Swift used to have breaking changes each major release, that's a non go for a big project.
3 replies →
Andreas Kling talked about it. It boils down to C++ interop sucks (no surprise for lang made by Apple), ecosystem is tiny, Rust works well enough with LLMs.
https://youtu.be/DbHjKi_jASY
2 replies →
This reads like cope because you're re-inventing RAII from first principles.
I cannot take this seriously as tutorials on robust Zig Allocation Pools will store a deinit method for each item within the pool, so when the pool deinits, all internal objects can be deinit'd.
That is just RAII & dtors from first principles, except with extra overhead of manually storing fat pointers yourself (and the bugs that come with this). Instead of using a language with builtin guarantees & optimizations around handling this so your object pools don't need to carry around a bunch of function pointers. C++ has aggressive de-virtualization passes so at runtime a lot of the 'complex object hierarchies' can be flattened to purely static function calls.
This is a general problem with destructors, you can't "batch delete" objects. To free a lot of stuff you're required to go pointer by pointer through the tree to clean up each object. To get real performance gains from pools you can't have per-object/subobject custom cleanup code.
Not necessarily. Drop semantics are just syntactic sugar, and can thus be aggressively inlined or auto vectorized by the compiler.
I've argued elsewhere some things that are wrong with RAII and C++ objects in general.
Here I would just like to mention that if you have to rely on "de-virtualization" passes, you're in a miserable situation architecturally. If you have code where the overhead of virtual function calls might be too much to pay, don't do virtual functions then. End of story.
To deconstruct a pool of objects, I don't see what should ever be wrong with a function pointer. The overhead of loading the function pointer will get divided by the number of objects being deconstructed. Care to explain what's the issue here?
> I don't see what should ever be wrong with a function pointer. [...]Care to explain what's the issue here?
1. You're writing code you don't have to
2. That adds runtime overhead
3. That when you screw up has non-trivial security & resource management side effects
This is objectively indefeasible in nearly any vaguely professional context.
7 replies →
I've argued elsewhere some things that are wrong with RAII and C++ objects in general.
You claimed there were problems many times for sure, I don't think you came up with any evidence of those problems.
6 replies →
Zig is always _less_ robust than rust. Even if you have a single allocation you can always forget to free it.
[dead]