Comment by plqbfbv
18 hours ago
Maybe I'm not that deep into programming, but I don't understand the hype about Zig?
I programmed in rust a bit and can't say I'm an expert, but in my view rust mostly-solved the memory management problem at compile time and without a GC, and it works very well. The biggest con and cost I've always seen repeated so far is that "it's slow to compile", and I get that, if you're past 250 crates the final --release link tends to become noticeable, but there were improvements to incremental compilation.
On the other hand - looking at the syntax from this post - Zig feels a blend of javascript, python and golang syntax that still requires memory management. So a nicer-written C that inherits all the issues from C? From the post: no functional programming, data mutation, memory leak, double-free, memory corruption.
Personally I'd rather trade a couple minutes of final link every time when this is the other option.
> I don't understand the hype about Zig?
As a long-time low-level programmer, and as someone working on a popular mainstream language, I find Zig fascinating, and I also think it addresses a long-standing problem in low-level programming. I'll get to the problem later, but the fascinating part is its use of partial evaluation (comptime) as a single coherent mechanism that replaces a myriad of other partial-evaluation mechanisms (macros, templates/generics, constexprs). That one mechanism is the core of the language, like macros are in lisps, and that design - whether you like it or not - is revolutionary. It's never been done before (other languages have partial evaluation mechanisms that are almost as general, but they're offered in addition to, not as a replacement of, other features).
> rust mostly-solved the memory management problem at compile time and without a GC
"Mostly" does a lot of work here because 1., if you look at the implementation of very efficient, possibly specialised data structures - the very thing you reach for a low-level language for - they typically require unsafe, and 2., it still suffers from the problem C++ has had for decades, which is that over time, as program changes and evolves over years, things tend to drift toward the more general mechanisms that rely on malloc/free on an individual objects, and the program gets slower and slower (huge runtimes like TCMalloc help, but not enough, because they can't move pointers). This problem, of programs that start out fast, but after five or ten years of evolution need to spend a lot of effort to remain fast, is one of the things moving collectors were designed to solve, but they require moving pointers, which doesn't work in low-level languages that are not meant to have an FFI layer between them and the hardware.
To compete with the performance of moving GCs, which allocate through bumping a pointer, like on the stack, and free memory in bulk, low-level languages need to rely on arenas (which work based on a similar principle), and Zig is the first language that makes arenas almost user-friendly and hopefully sufficiently composable to withstand program evolution. Of course, time will tell how well this works in practice.
> partial evaluation (comptime) as a single coherent mechanism that replaces a myriad of other partial-evaluation mechanisms (macros, templates/generics, constexprs).
So sad dlang never gets the credit it deserves. None of these ideas in zig are novel.
Yes. I don't really understand why Zig gets more attention than dlang. It seems that it is more to do with the personality of the authors, rather than the technicalities. I get the impression that Walter is more laissez-faire, while Andrew is more single-minded. I'm grateful to the contributions both have made.
> the very thing you reach for a low-level language for - they typically require unsafe
There's a formal proof asserting that if you keep up the safety invariants within an unsafe region then that will not infect other code, even in the presence of arbitrary other correctly-written unsafe blocks.
This means you can build abstractions on top of these low-level primitives to keep it contained, so consumer code never has to even think about or know there's unsafe blocks in it. The type system lets you build very powerful abstractions so these go a long way.
There's a lot of woo-woo scare quoting around how much you actually have to use unsafe code in Rust. It's fairly uncommon to actually have to reach for them in practice. Most of my usage ends up being things like converting a &[u8] to a &str when I know it's already valid UTF-8 so I want to skip the linear-time validity check. Very rarely do I have to build data structures with complicated pointer juggling, because there's often a library that already does what I need!
> which is that over time, as program changes and evolves over years, things tend to drift toward the more general mechanisms that rely on malloc/free on an individual objects, and the program gets slower and slower
What are you talking about? I've never encountered this and I've been using Rust for 10 years.
I works in pretty low level OS code. I promise you most of our code would be unsafe. And using unsafe in rust is less ergonomic then using zig or c++.
We could use rust. But it wouldn’t give us anything.
1 reply →
> > which is that over time, as program changes and evolves over years, things tend to drift toward the more general mechanisms that rely on malloc/free on an individual objects, and the program gets slower and slower
I think the idea is that a small program can organize its allocations and data structures to minimize number of calls to malloc, e.g. with preallocated workspace structs, or slab allocation, and similar approaches. But as a program gets bigger, there's a pressure to have looser coupling, to have subsystems with simple convenient APIs which leads to them doing on-demand malloc calls internally, rather than having consumers pre-allocate their needed workspace. Because that kind of workspace management results in more complex APIs and more burden on the consumer.
That said, I don't really believe it either, at least for the kind of codebase where it would matter (scientific computing, in-memory DB server, etc). A codebase that places an emphasis on minimizing heap operations in hot codepaths can do so by consistently using workspaces and allocation-avoiding APIs. I don't think it's so difficult really, but it does take a conscious design decision to do so. But writing something like a web browser in this way could be annoying due to most data having wildly variable sizes, and zig's arena concept would be very handy -- but rust has crates like bumpalo for that purpose.
My personal mantra: "Think in FORTRAN, code in Rust/Julia/C++". But I'm mostly working on HPC-style code where I don't have to do with wildly varying input or output sizes.
6 replies →
That's always been true in all the safe languages with unsafe escape hatches, except here these "primitives" are the main reason to reach for a low-level language in the first place - because they presumably require the control that low-level languages offer. Combining them in the same language might appeal to some and not to others who think that the high-level, safe parts are unnecessarily complicated because it needs to integrate with the low-level parts, and the low-level parts are unnecessarily complicated because they need to integrate with the safe parts. Anyway, some like this and some don't, but my point is that it's not "mostly solved".
> What are you talking about? I've never encountered this and I've been using Rust for 10 years.
Okay, but I've been doing low-level programming professionally for 25 years, and have encountered this over and over in large programs (over 500KLOC) as they evolve.
6 replies →
> There's a formal proof asserting that if you keep up the safety invariants within an unsafe region then that will not infect other code, even in the presence of arbitrary other correctly-written unsafe blocks.
In general "unsafe" does not compose.
"if you keep up the safety invariants within an unsafe region"
This condition is doing a lot of heavy lifting.
8 replies →
C++ solved the memory issue at compile time too. I don't understand why even use Rust when modern C++ can do everything you can do in Rust but better and more ergonomic. New languages to solve old problems.
Basically you pick the type safety that Modula-2 or Mesa already offered in the late 1970's, repackage it with comptime and more C like syntax, and have a whole legion of new devs jumping into it.
Note that AT&T, where UNIX and C were born, the language they were researching as C replacement was Cyclone, not something that is not much different.
Rust could not work in a world where compilation was a single threaded affair. I think it exists now instead of in the 90’s and 00’s in good part because of this.
I don’t think it’s an accident that it has succeeded as multicore took over. We are now well past a point that a task that can be split with 70% efficiency into multiple parallel tasks is 5-10x faster than the optimal sequential solution. Expensive multicore machines existed when Rust was a baby but it didn’t really catch on until 4 core was common in consumer hardware. And now I have an ancient laptop with 16 cores.
But Rust is also good for producing correct code to run on those systems. So it benefits twice.
As someone who primarily writes in TS, Go, Python, I agree with your assessment: it looks like those languages. The reason I like Go is the reason I like Zig: the language is relatively simple and feels like C.
The C interop is a huge win as someone who wants to do more posix/wayland projects.
Zig is aiming to be lower level than Rust. As the project homepage prominently advertises, it has no hidden memory allocation or control flow. It also gives more control over how memory is allocated, which is potentially useful in applications with particularly tight performance requirements.
Kelley first created Zig when hr was working on a digital audio workstation and found most existing languages to be awkward for working with particularly hard real time requirements, but still wanted something more modern than C. Im speculating here, but I believe its advantage over Rust for that specific application is that you have tighter control over exactly when memory is allocated and deallocated, and how data is laid out in it. Rust wants to tie allocation lifetimes to scope in a very fine grained way that I would guess is beneficial the vast majority of the time, but does still make it harder to reason about when you’re about to stall out the CPU while the allocator does its thing.
> it has no hidden memory allocation or control flow
Rust had like 3 allocating types total. If you aren't working with extremely deeply nested 3rd party types it's trivial to identify when allocations happen. Hell you could throw a lint rule together in like 5 minutes to warn on it if you're really worried. Besides Drop (excluding async) is there even any hidden control flow?
> Im speculating here, but I believe its advantage over Rust for that specific application is that you have tighter control over exactly when memory is allocated and deallocated, and how data is laid out in it.
Rust has almost exactly the same semantics for controlling allocations and deallocations, it just prevents you from screwing it up and not freeing something or using the allocation after freeing it. You still have to pass around your reference in your call stack until you no longer need it.
> Rust wants to tie allocation lifetimes to scope in a very fine grained way that I would guess is beneficial the vast majority of the time, but does still make it harder to reason about when you’re about to stall out the CPU while the allocator does its thing.
It's really not substantially different. You allocate ahead of time or don't allocate at all. The only real difference is you might want to use an Option instead of an uninitialized pointer because it's semantically more correct and harder to screw up.
But here I feel like we’re at risk of heading down the same old doom spiral that plagues any conversation about programming languages when people try to treat it as a competition: getting pedantic about what’s technically possible in a language. It’s much more interesting to talk about how a language wants to be used.
So, in the case of Zig, every function that wants to be able to allocate or deallocate heap memory needs an explicit reference to an allocator. That means that you can tell whether a function might allocate memory from its signature. It also means that changing a function so that it can allocate is explicitly a breaking change.
That’s a really interesting design decision. And the reasons why someone would or would not want something like that baked directly into the language are so much more interesting than bickering about how technically with proper discipline you can have that kind of control in any non-GC language.
5 replies →
I wish people would stop inventing new languages for nostd when we have nostd
> Rust had like 3 allocating types total.
Seriously? Categories of types maybe, but literal types it's more than that.
Even closures allocate if they need to capture their environment.
1 reply →
> Zig is aiming to be lower level than Rust.
Zig and Rust are equivalently "low level". Zig isn't any closer to the hardware than Rust is.
> but I believe its advantage over Rust for that specific application is that you have tighter control over exactly when memory is allocated and deallocated, and how data is laid out in it.
Rust gives you all this, too.
Zig's primary (possibly only) advantage over Rust is that it has much faster compilation times.
Comptime is a big one too. You can achieve similar things in Rust with generics and macros, but comptime makes certain things easier (and other things harder).
.map is definitely not low-level
The main advantage of Zig for me and many other programmers, is the simple mental model you need to keep while reading the code. All control flow is expressed by keywords, never symbols. That along makes code much more readable. There is nothing like Rust macros, things can't do "magic. Zig comptime are regular functions, so you can clearly see and follow the trail. There are no exceptions, no recoverable panics, all code will run linearly as you see it written. Error handling in Zig is close to perfect, in my view. Obviously, Zig is not a language to use for high-level jobs. It makes no sense to write CRUD web app in Zig, you will be fighting the resource management for no good reason. For me personally, I feel that I'm too stupid to use Rust effectively. I started moving away from C++ purely because I don't want to deal with exceptions. And Rust is just a more complex C++ with memory safety.
Zig is exciting to people that still actually like C. Is that a rationale choice? Not very often. Is it a wrong choice? Also again not very often. At least, not for anything in scope of a solo dev.
The industry where it seems strongest positioned is embedded. Will it actually break into that domain? No idea.
One of the things I liked about Zig is that it made choosing which memory strategy (malloc, stack allocated, Arena, Bounded allocation, combo) you needed for a particular code second straightforwarded and well supported.
Microbenchmarks are cool, but better algorithms are where you get faster code.
> So a nicer-written C that inherits all the issues from C?
And inherits all of the benefits of C. C is the foundation of the computing world. "C, but not built 50 years ago" is, by itself, a tremendous value add to a programming ecosystem that has largely abandoned attempts to write a truly performant language in favor of handicapping programmers with fully automated safety.
Rust is a low-level language for people who don't write low-level code. Zig is for those who do.
If you're writing ultra low latency code you basically want everything allocated from an arena, with different arenas for different kinds of objects. Zig comes with this built in, while Rust makes it extremely unergonomic to do safely (due to the lifetime system).
Arenas in Rust work very well with lifetimes. In fact arenas benefit greatly from lifetimes, because lifetimes allow them to uphold the usual Rust safety guarantees about preventing use-after-free. For example, here's the bumpalo crate in action:
yeah to me zig exists as a counter-reaction to rust. which means avoiding both the good and bad things rust does. and rust does a lot of things right, so...
Rust is for devs who think "if only c++ had a few more features, it would be perfect". Zig is for devs who think "if only C had fewer features, it would be perfect"
ironically rust has fewer features than c++ and zig has more than c
1 reply →