Hacker News new | past | comments | ask | show | jobs | submit login
Revisiting the design approach to Zig (sourcegraph.com)
134 points by sixhobbits on Sept 6, 2022 | hide | past | favorite | 94 comments



Even though professionally I use very high level languages like Clojure and Common Lisp I always enjoy coding in C for my hobby project. Something about the simplicity just helps me calm. But there will always be a point where I have this nagging feeling I make everything more work than necessary. Recently I have been playing around with Zig. It gives me the seem feeling of calm from the simplicity but without the nagging feeling. Everything just seems to work and flow with Zig, I thoroughly enjoy coding I it.


Every time I hear Andrew speak I consistently find his judgement and stylistic preferences to be similar to mine. I would love to use Zig for work some time.


You might guess that something changed from "revisiting" in the headline, but there's nothing particularly new in the article. It describes the same Zig features we've been hearing about all along and they're probably why you've heard about Zig in the first place.

To talk about one of them, it makes sense that Zig ignores compile-time branches that aren't chosen because otherwise they wouldn't be a replacement for ifdefs. However, I wonder if some kind of abstract interpretation mode that checks both sides of a branch would make sense?


This article praises everything Zig does and lists no negatives of its design. I'd recommend a different article for a balanced take, particularly regarding comparisons with Rust: https://www.scattered-thoughts.net/writing/assorted-thoughts...


I don't trust any language comparison one-on-one with Rust, because fans of Rust today are just like fans of Python 20 years ago: "this language is the greatest accomplishment of humanity, easily," if I had to sum up the vibe I'm talking about.

compare [language] with C, fairly and accurately, if you want me to believe a word you say.


That's funny, I read this affirmation (and variants thereof) rather often, but I've been using Rust on and off since 0.4 and I've always been discussing with developers who have balanced and careful opinions of Rust, starting with the Rust language and compiler teams.

Rust has never been publicized as a solution to all problems. It happens to be my current favorite language for many tasks, but I don't think any serious Rust developer harbors the impression that Rust is designed to implement everything.

That being said, the community is much larger these days, so I assume that there are users who do not have a sufficient PL background to understand the tradeoffs made in the design of Rust.


> Rust has never been publicized as a solution to all problems.


As a Zig fan, I thought GP's link was pretty favorable to Zig and pretty likely to be informative to users of other languages.


There is not many languages that would fair worse next to C. For starters, not being able to create any form of even minimal, zero-cost abstractions like a vector is criminal.


I think it's an apt comparison, Zig vs. C, given that Zig intends to replace C.

Go was aiming to be a replacement for C as well, but they didn't even try to reach that goal because Go programs require an OS to run on; something must host the runtime, even though it's included in the binary. I love Go and I dislike C quite a bit.

Zig vs. C seems fair to me, because there are no Zig zealots (or very few, at least not yet) and there are no C zealots who have lots of experience with C.


This 100%

I’m darn sick of reading articles that don’t consider tradeoffs. Engineering can be defined as “a search for the optimal set of tradeoffs”. But, it seems like every article I ever read about new tech only discussed pros, not cons. I don’t get it. The cons are just as important, if not more so. Unfortunately, for some reason, people associate cons with flaws/weaknesses/failures rather than just a “point on the tradeoff curve”. Often: no perfect solution exists and those claiming otherwise are being dishonest. I would love to see this culture change at least for engineering literature where we should know better.

All that said: I DO think that Zig has selected a reasonable point in the tradeoff curve. I just really want to understand that point better. How am I supposed to use it most effectively if I don’t understand it?


Here is the question I think that you need to answer to choose between Rust and Zig:

If your dependencies wrap more than 2 external libraries that have poor architecture, you're about to have a miserable time in Rust.

Rust creates a lot of friction if an external library has a poor architecture. And, by default, most libraries have a very poor architecture. This leads to things like:

"Giving up on wlroots-rs"--http://way-cooler.org/blog/2019/04/29/rewriting-way-cooler-i...

Talk about the issues of RLua--https://www.reddit.com/r/rust/comments/biq864/giving_up_on_w... "The thing that was SO tiring about writing this for me wasn't Rust, it was reasoning about the C library that I was binding. I'm just... tired of using C APIs! I'm tired of not having the compiler reason for me, I'm tired of having to second guess at what invariants the API expects and guess at what of a myriad of interesting non-local behavior a function call will do. I know that the Lua C API is a really, especially bad example, but I just don't know what the general solution is here."

Consequently, this is where you get "Rewrite All The Things In Rust!" What's the point of fighting with Rust over all these nice safety guarantees and then throwing them out at the API boundary? If you can keep everything in Rust, this simply isn't an issue.

This is where Zig shines. It catches some of the main classes of bugs but still lets you talk to libraries with crap architecture. Zig gets the fact that infrastructure and ecosystem matter--unlike everybody else operating in the C space (the cross compiling is phenomenal, for example, because they put a lot of work into it). Zig is doing some interesting things, but it's mostly some very solid choices with a lot of elbow grease applied. And that's worth a LOT.

As an aside, I will also say that Zig feels better for embedded programming than Rust. Rust feels like it has stalled in embedded areas (things like sized integers, bitfield packing, placement construction of data structures, statics and globals that don't have locking overhead, lack of do-while, etc. although I would like to point out in Rust's favor that Rust finally got label-break-value which is a godsend when porting embedded code with goto error handling) while Zig has explicitly targeted those from the beginning. That makes Zig feel better for embedded from an ergonomic perspective, to my (obviously quite subjective) taste.


I also agree that Zig has more promise for embedded than Rust, in my opinion. I use Nim daily for embedded firmware development for similar reasons that I like Zig, but Nim has some baggage that can get in the way at times around it’s compiler (though fixing those issues is surprisingly accessible I’ve found).

That said, until someone works out how to properly integrate Zig or Nim or etc. into CMake, then there’s always going to be friction. Too much of the IDF world relies on it and it’s quirks.

In Nim land I get around this by --compileOnly to C sources, which works but is a little cumbersome. Rust is rebuilding the embedded world from scratch, which is a non starter for my work. I do have hope Zig will have a better story than both for this, but we will see!


> I would like to point out in Rust's favor that Rust finally got label-break-value

Oh, interesting, I was wondering if this (breaking with both a label and a value) existed (in a different context), and I concluded it didn't exist and so probably nobody had wanted it, but turns out it's in the next version and I didn't hear about it.

I don't understand a desire for do-while, I can't imagine what I might write where that looks reasonable and the loop equivalent in Rust is not very good.

If all you wanted was nicer C then yeah, Zig is that and Rust isn't -- I just don't think "nicer C" is a reasonable goal any more.


> I don't understand a desire for do-while

It has to do with porting code, generally. If I'm writing original Rust, I rarely miss it. It's not a huge deal, but you bump into it juuuust often enough to (probably unfairly) curse Rust out when you hit it yet again.

Here's code from CMSIS DAP debug probe firmware from ARM: https://github.com/ARMmbed/DAPLink/blob/main/source/daplink/...

Note the nested do { do { } while (); if (err) break;} while(); if (err) break;

Should that code be rewritten? Most certainly it should, and it should be given a proper Error type. However, when you are first porting it, you need to match semantics or you get a bunch of off-by-one, missed error, or missed end of stream bugs. And, as you point out, the Rust loop{} equivalents suck.

We've all written suboptimal code, and we all live in a suboptimal world. :)


I definitely don't miss it while writing my own code. In porting well, I think because we've got volatile flag nonsense, we need to do some significant surgery to even make this compile anyway, and so perhaps

  do { ... } while (((data & DAP_Data.transfer.match_mask) != match_value) && match_retry-- && !DAP_TransferAbort);
becomes something like

  loop {
    ...
    if data & DAP_Data.transfer.match_mask) == match_value {
      break;
    }
    match_retry -= 1;
    if match_retry == 0 {
      break;
    }
    if check_transfer_abort() { // Replaces DAP_TransferAbort
      break;
    }
  }
I think that's actually more clear, and my mental focus is DAP_TransferAbort. I think it's just C programmers being C programmers, and we just want AtomicBool for this but I'm more comfortable wrapping it in this check_transfer_abort() function while I investigate whether anybody thinks this is a counter, or there's actually physical hardware backing it despite the way it looks like it's just a global variable.


This is one of the things I happen to agree with you. :)

If we take comptime feature away, Zig is basically Modula-2 (1978) with friendlier syntax for folks with C background.

Everything else in terms of security Modula-2 did better (yes it did have null pointers, but supports null checking).

And the tagging mechanisms for use-after-free are anyway available for C and C++ compilers for at least 20 years (Insure++, PurifyPlus and VC++ 6.0 debug allocator being part of the first ones).


The Lua C API is now bad because you can't easily make Rust bindings to it?


Well zig could be a no tradeoff improvement over C while rust definitely has tradeoffs. So I think "all good" is a plausible assessment.


Everything has tradeoffs. Be careful, lest the "Zig Evangelism Strike Force" becomes a thing!


A bit pedantic, but a trade off with any new programming language is “having to learn a new language”. I haven’t programmed in zig but the syntax looks different enough from C that it would take a while to pick up.


I watched the video rather than read the short post. It did a good job getting into details that illustrate some of the tradeoffs. e.g. Do you want a small language that you can fully learn, or do you want a language with many features that you can spend time learning?

Other things that are made explicit such as specifying an allocator on every allocation is an inconvenience that you pay for that flexibility. The choice of safety being added rather than assumed and later satisfied or declared an unsafe area is vastly different with a tradeoff in how developers use the language. An example given was how the self-hosted complier can use cache optimized untagged unions with the tags in a separate array.

If I had to guess what the single biggest pattern of tradeoffs is, I'd say it's about being explicit rather than implicit. You gain fine-grained control at the cost of brevity/convenience.


>we can't type-check zig libraries which contain generics. >comptime is_odd_perfect_number(n)

I assume zig has a finite bound on comptime_int, so we can in fact always know what is_odd_perfect_number() returns.


You misunderstand.

> we can't type-check zig libraries which contain generics. We can only type-check specific uses of those libraries.

I.e. you cannot say whether this compiles for all n, only for the instances you actually instantiate


Right, and this should be discussed a lot more when talking about Zig, because I suspect this is going to be a significant burden at the ecosystem level:

- library users will find compilation errors in the library code (his is annoying, but when you get used to it, you can treat it as any kind of library bugs and submit a PR)

- library users are going to rely on accidental behaviors (that is “in the library author's mind this isn't been used this way”) and then you'll have breaking changes without noticing.

(I say that as someone currently working on a DSL for the train industry, and this language (started in 2012) has something really close to Zig's comptime and we're currently trying to move away from it, because of the aforementioned issues. And it's actually pretty hard because big chunks of the existing code don't actually compile unconditionally and only compile when a given set of compile-time conditions are met, which makes the migration process hard)


C++ has this problem too, but the funny thing is that I rarely run into it in practice.

There is a lot of templatized c++ library code out there, yet in everyday practice I find it's very rare that I need to instantiate a template with a type that causes a compile error.


> C++ has this problem too

That's true. C++ templates are exactly like that too.

They are a bit arcane though, and many developers are afraid of them and use them very conservatively or even avoid them altogether so maybe this helps mitigate the issue.

Comptime (at least with the language I'm currently working on) are a lot less scary, and people use them quite a lot without thinking too much, so maybe it's the main difference. And I think Zig is closer to this than it is to C++ templates, but I can't be sure since I've never seen people writing Zig code.


I am afraid of being robbed in a dark alley, now in regards to programming languages I never understood what one should be afraid of.


I've fortunately never been robbed in a dark alley and that's not something that sounds like a real threat to me, but I had to intervene on nightmarish code-bases and some “convenient” feature sometime turn to be real curses (JavaScript `with` anyone?).

Edit: it's late in here, and I completely missed your point. I'm personally not usually afraid with language features, but some requires more work than others to be accustomed to, and not everyone is willing to spend their spare time doing so.


Lucky you :)

I used to code in C++ and I had that problem quite often. Including in implementations of the STL.


I suspect this might not be a problem for Zig long-term. One can imagine a blend of comptime, concepts, and generics which which can help mitigate (and often solve) these kinds of problems. When combined with compiler enforcement (or even a community-adopted linter) this could help the library author detect problems while still being able to use comptime's unique benefits.


I fail to see how this is a problem. The compiler ensured that the compiled code will always work.

And if you put that in you API and someone call foo with an odd perfect number, well it will fail to compile.

It's conditional compilation, exactly what C does with macros. Except here, you don't have to learn an awful and honestly, sometimes, incomprehensible parallel language.

Zig has some shortcomings but I fail to see how this is one.


It's not the end of the world, but there are significant disadvantages. For example, it makes it difficult for generic libraries to expose stable interfaces. The interface is 'use any type that doesn't make this exact implementation break', rather than (e.g.) 'use any type that implements these traits'. So you could potentially upgrade from v1.0.0 to v1.0.1 of a library and find that instantiating a generic function with a particular type no longer compiles. If you want to know why it doesn't compile, you'll just have to dig into the implementation of the library.


Ok I get you. The absence of traits (or concept) make documentation and reading the API harder. You see a `comptime T` somewhere and you don't what is expected of this type unless you either read the code in full or just try your type and see if it compiles.

That's a valid point indeed and there's an issue for that: https://github.com/ziglang/zig/issues/1268


No standard utf8 string library is scary - I would happily learn all the mem gymnastics to make this work but fiddling with competing UTF8 libraries as is the case with C/C++ is just not my cuppa.


There are functions for handling utf8 in the standard library.


Well, there's functions for working with code points within the bytes that (may) make up utf8 strings.. in order to do actual string level manipulation a library is needed (such as https://github.com/jecolon/ziglyph or others)


That’s what I felt as well when I read it. Maybe it’s as industry-redefining as it claims to be, but surely there must be some tradeoffs.

Rust’s tradeoff in providing safe and faster code is mentioned. Why not Zig’s?


Having played with Zig I would say the tradeoff is complexity. Zig is harder to learn than C with more difficult concepts. Something like Go is much quicker. Yet I think Zig is finding an okay balance. You can still get productive much faster than in Rust.

Manual memory management gives obvious downsides which should be well known. Zig does this in the best way I have seen though.


> Zig is harder to learn than C with more difficult concepts.

I'm using Zig and C for a good amount of time now and i disagree. Zig has simpler concepts as C, but in C you can just ignore them.

Consider

   char buf[4];
   float * f = buf;
   *f = 32;
Seems simple, compiles, and even executes. Everyone is happy. But that code will just explode randomly on non-x86 platforms as it violates alignment rules. In Zig, this code will give you a compile error, as []u8 does have alignment 1, while a []f32 will have alignment 4. This means you have to learn about alignment before accidently writing code that will randomly crash at runtime depending on the stack position of buf.

And Zig has a lot of these things that require implicit knowledge in C as explicit knowledge implemented in the language itself. Same goes for integer casting, shifting more than sizeof() bits, and so on.

It moves the footguns from the code to the user, and so the user has to learn about all of these concepts before writing Zig code. In C, the code will silently compile and misbehave at runtime. This might feel like additional complexity in a lot of places, but the complexity is there in both languages. One just makes it explicit.

But this is obviously not true for all things in Zig. comptime is a whole new feature that wasn't seen in a lot of languages before and afaik not as it is used in Zig, so it's something new to learn even if you already can do perfect C. The same goes for more precise data types (struct vs extern struct) and so on.

One hard footgun in Zig right now is implicit aliasing of parameter types, which sometimes get passed as a reference and sometimes as a value. This is a known footgun is already on the table to be adressed by better semantics and more safety guards

For me, writing Zig feels way less complex than C because a lot of brain load that went into writing correct C was offloaded to the compiler, which is good. But it also increases the friction when writing code as the compiler forces you to think about alignment and such.


Well, yeah the article is meant to promote it. They had podcast and everything. Same with Rust podcast.


Yes, I think we should understand that this is what Andrew says, so e.g. "This type of optimization, Andrew explains, would not be possible in languages like Rust" is akin to when football players tell you they're confident they can win. Of course they're confident they can win, but at least half of them are going to be disappointed.

I actually don't know what Andrew is getting at with that particular case though. If the situation is that the types are obvious, so the Zig isn't ever looking at the tags, the Rust won't look at the tags either. Worse, I don't think Zig has niche optimisation, so there are a bunch of cases where Zig has to choose, have tags (more space, slower, but same safety as Rust) or go without (same size as Rust, but less safety). Rust only guarantees the niche in certain narrow cases like Option<&T> (is the same size as &T) but it actually delivers a lot more than that, and this continues to improve.


> There is no global allocator in Zig

> Zig has a global allocator that you’re encouraged to use

wat


This is sadly very badly worded.

Zig provides a global allocator when running tests that will check for memory leaks and double frees. This allocator is only available in the test suite tho, if you want to compile an executable, one has to chose between several allocators and instantiate them explicitly.

There are three allocators that are globally available in the standard library: - `std.heap.c_allocator`, a wrapper around malloc/free. only available when linking libc - `std.heap.page_allocator`, a wrapper around VirtualAlloc/mmap. Can only allocate in OS page size granularity and is quite slow. A good backing for other allocators tho. - `std.testing.failing_allocator`, an allocator that will always yield OutOfMemory.

For the use in an application, there's the std.heap.GeneralPurposeAllocator type that needs to be explicitly instantiated, and it provides a good portion of debugging features right now. High-speed implementations are planned for release modes


There is no hidden implicit allocator used by the language or stdlib (like in Rust or C++), instead every stdlib API that allocates requires an allocator either in an initialization function, or as argument on individual functions.

The stdlib then provides a variety of allocator implementations that can be plugged into those allocator arguments.


To add to that, it's also idiomatic for external libraries in Zig to take an allocator as arguments, rather than using the libc's malloc like a lot of C libraries do.


Which as an aside, is a lovely feature when you’re doing things like working with external SPI PSRAM and rightfully want to treat it differently to your local SRAM in embedded applications.

We didn’t use Zig for our firmware, but we did come up with a similar approach in Nim (basically a neat type that takes allocators/frees/reallocs and handles managing the ref count so you can use the variable as any other Nim variable. It’s not perfect, but close enough for our purposes)


You left out a whole paragraph of context to the second statement, and especially chose to cut off even the beginning and end of that sentence:

> By convention, Zig has a global allocator that you’re encouraged to use for all your unit tests

If you read sentences in bits and pieces by themselves, you'll find that it does lead to a lot of misinterpretation.


I suspect this means that the Zig stdlib does not use a default allocator but Zig provides a default allocator that you have to intentionnally use in your functions if you want to allocate. Is this correct ?


This is correct, with the caveat that I believe zig provides several different allocators that you can pick and choose for various situations.

so the word 'default' may not necessarily apply the way it doesn't apply wrt to 'default pointer' in C++ because it offers both unique_ptr and shared_ptr with very different behaviors and semantics.


Came to point this out... I don't think I trust these people to write my compiler.


As for this fragment:

> Zig is faster than C. This is partly a result of Andrew’s data-oriented design approach that led to performance-enhancing changes in Zig programming that would not be possible in other languages.

Can anyone comment on this claim? It's quite bold given that the competition usually aims to get close to the speed of C; getting faster would be an amazing feat regardless of syntax used.


If "faster than C" means "I can make a Zig program such that no equivalent C program is faster", then that's obviously false. If it means "some idiomatic Zig program is faster than the equivalent idiomatic C", it might be true.


That is a good point, but I think "idiomatic" here deserves some elaboration. Often people use it in very vague way: this is what (outspoken) programmers agree is the best way to do this thing. In this case, I think a more limited and specific definition is sufficient: don't use escape hatches. Don't use "unsafe" or compiler-specific extensions or inline assembly or libraries that were actually written in assembly. What can you do in the language as it comes out of the box, as every tutorial would lead you to do? I think Zig could indeed be "faster than C" in this sense, as others have pointed out Fortran can be, but Rust might not be able to.

There's also an important lesson here about optimization BTW. Everyone thinks of optimizing algorithms in terms of big-O stuff, and that's great, but sometimes data size or layout or frequency of pointer-chasing or mispredicted branches can actually be the real difference makers. Cache-friendly vs. cache-hostile code can be particularly hard to spot, because it often doesn't show up even in most kinds of profiling. It presents only as a sort of general performance degradation across the board unless you're specifically looking for it. I've implemented significant performance gains just by rearranging code and scheduling to make better use of a processor's L1 I-cache. The gains to be had from doing the same for data in L2 and L3 caches are even greater. I appreciate that Zig is being designed with these issues in mind, because it seems like a lot of other newer languages are being designed with the specific goal of defeating modern computers' performance-enhancement techniques.


> If "faster than C" means "I can make a Zig program such that no equivalent C program is faster", then that's obviously false.

It could be true though, because compilers aren't perfect, and for instance it actually happens with scientific code written in Fortran: C has restrict but because it's not widely used, C compilers don't make use of it as much as they could for optimization, which means that a language like Fortran that values the non-aliasing guarantee can have a compiler which optimizes it more than what C compilers do.

That being said, there's nothing preventing C compilers to add the same level of aliasing-based optimizations in theory, it's just that they don't do it yet in practice (Rust is pushing the boundaries on that for LLVM though, because noalias optimizations is regarded as a promising performance enhancement for Rust).


A concrete example of idiomatic Zig being faster is MultiArrayList and language features that work well with it, which lets you use the struct-of-arrays pattern in an ergonomic way.

You can do it in any other systems language, of course, but the Zig syntax is very convenient so arguably it's more likely to be used.

https://zig.news/kristoff/struct-of-arrays-soa-in-zig-easy-i...


> “I can make a Zig program such that no equivalent C program is faster", then that's obviously false

Why would it be obviously false? C is not that low-level of a language, for example it has no native way of doing SIMD - so Rust, C++ (and I guess Zig as well?) all have programs that are simply non-expressible in C (inline assembly is not C) and are possibly orders of magnitude faster. But there are million other things “hard-coded” into C where with more control faster execution could be achieved - e.g. one might want to have better control over stack usage.


Sure, but this is only true because C allows for assembly code so you can always write assembly that beats zig.

If you limit it to actual C and don't include assembly, it's possible the statement is true but I would expect it to also be false depending on the circumstances.

zig is going to have more opportunities to optimize but may be paying a cost in terms of offered features that C doesn't have.

But in general, yes, zig is very fast and competes with C in terms of performance.


Inline Assembly is not part of ISO C and plenty of languages have such extensions.


If you read closer, my point was that anything Zig can do, can be done in C due to it's ubiquitous ability to bypass the compiler and move straight to assembler.

I can't think of a major C compiler that does not allow for this, can you? The only thing I could imagine coming close is something like compiling to a very specific target such as GPU shaders, but even that's a bad example considering shaders have been standardized.


MSVC, inline Assembly is only available on x86 mode, everywhere else requires compiler intrisics or MASM.


Just checked, apparently that's right for MSVC.

My point still remains but I take your point about its limitations. My post was more a caveat on the original point anyways, I wouldn't expect most people to drop down to assembly except in very specific circumstances.


assembly is not the ultimate in speed, that's a myth. for many problems there are often high level approaches that offer orders of magnitude of speed up which are simply not expressible in assembly code due to the complexity of the assembly that would be needed to express them making it infeasible to program these approaches in assembly.


You can express stuff in assembly that's simply not possible in a high level language like C (or Zig for that matter). An obvious thing is defining your own calling convention with hardwired 'register semantics', and then changing that calling convention at will. Or you can use status flags as additional function return values, manipulate the stack, jump into the middle of an instruction, write self-modifying code etc etc (disclaimer: some of this stuff might actually not be possible on modern CPUs though).


yes, of course, but there are ways to reason about programs that are not expressible in assembly, so a purely assembly program is not able to reason about itself in this way in order to optimize itself in the same way as a higher level program can. both domains can do things the other can't. and, again, the more complex a concept is, the more difficult it is to implement in assembly, due to the sheer volume of code. you can't argue with that.


This is called moving the goalpost.

You started talking about speed, now you're talking about "expressiveness", which is a wholly different thing.

Stay on topic.


no, that's exactly what I was talking about in the first reply, i just reiterated it, because your reply wasn't taking any of that into account.


Then you went for a strawman because the discussion was about performance, not expressiveness.


No, my original comment is about performance, and out of that follows the necessity for expressiveness. Compilers and runtimes can perform more powerful optimizations when they have more semantics available to them. Meanwhile assembly is just a bunch of homogenous gunk to optimizers. At least until decompilers get way smarter than even the best ones are right now. Even IDA / HexRays don't reason about code at a level high enough to propose safe optimizations that can rival such optimizations as runtimes and compilers can perform when dealing with semantically rich code.


> ...Meanwhile assembly is just a bunch of homogenous gunk to optimizers

I wouldn't expect an assembler try to apply any further optimizations to my assembly code, that would be indeed quite pointless.

For any specific problem, a human writing assembly code will almost always be able to exploit weird loopholes in a specific CPU instruction set that a compiler for a high level general programming language won't be able to (unless we're going into the realm of superoptimization via brute-force trial-and-error where an optimizer may eventually arrive at the same tricks that an anarchist assembly programmer would do).


anything expressible above assembly is expressible in assembly, both at the macro and micro level, only assembly can hyper-optimize the use-case in a way that a generalized compiler often can't.

There is no myth here, it's a simple statement of fact.

The Zig compiler does things C compilers can't. C developers can get access to those same techniques by bypassing C compilers to express them directly in assembly.

This is not arguable, it simply is.


> anything expressible above assembly is expressible in assembly

parser for unicode, go. I'll wait over here.


If I grab a C library that does this, compile it, and hand you the resulting assembly, will your mind explode?


that's not written in assembly, just compiled to assembly.


When you start trying to argue semantics, you've given up the point.


Just restating my argument to you, so you can better understand it.


If you mean "parsing UTF-8 into codepoints", it isn't that complex, and the most optimized libraries already use SIMD intrinsics which are basically assembly instructions.


You are aware that 30 years ago we used to write business applications in pure Assembly, right?


we did? I was a programmer 30 years ago, and what was being widely used was cobol, qbasic, pascal, even ada, meanwhile assembly was already at this point relegated to optimization (in non-business applications) or nothing (in business applications, where structured programming was a line manager's mantra, just like OOP was later, then FP, and so on)


>cobol, qbasic, pascal, even ada

Except for QBasic, all of them compile to machine code and have features that can't be represented in idiomatic C, but are often trivial to implement on the underlying hardware.


No arguing that. It still doesn't mean that writing stuff in assembly is better than leaving it higher level. Nowadays we have new approaches that didn't exist back then. Look up any JavaScript optimization techniques to see stuff that's simply not possible to do once your code is moved down to assembly because it loses semantics. Or look at fusion and type erasure in Haskell. In my original comment, I wasn't arguing about the state of things 30 years ago.


For example, Lotus 1-2-3.


Lotus 1-2-3 was a legacy code base already 30 years ago. Which was one of the contributing reasons to why it eventually died.


I can provide examples, PC Tools for one.

I didn't wrote that every developer out there was writing 100% of business applications in Assembly, and I guess I don't need to explain the difference between ∃ x and ∀ x.


I'm guessing that the author is just confusing multiple things.

Andrew has been using data oriented approaches to make the self hosted compiler fast. And the article mentions an example where he's separated (presumably arrays of) tagged unions into distinct arrays of tags and payloads. Depending on the access pattern, this can definitely speed things up on modern CPUs.

The article then goes on to say that this is impossible in Rust, which is probably a fair statement. You may be able to get something quite close with really horrible, unsafe code but no sane person would try.

But it's blatantly untrue that you can't do this in C. I mean, C doesn't even have tagged unions so you need to code the tags explicitly if you want them. Separating them into a different array is trivial.

I guess it's possible that one could argue, say, Turing idiomatic Zig into highly optimised Zig is easier than the equivalent in C so, in practice, equivalent code is faster. Or some such. I have no idea if anyone's actually shown that quantitively though.


> The article then goes on to say that this is impossible in Rust, which is probably a fair statement. You may be able to get something quite close with really horrible, unsafe code but no sane person would try.

As it happens, a crate was just released that does exactly this, using a derive macro [0]. (I wouldn't recommend using it quite yet, though; it still has a few kinks to be worked out.)

[0] https://crates.io/crates/peapod


Yeah, in general macros allow for Rust to do these things just fine. I wouldn't say that it's a fair statement at all.


The data oriented design stuff was mostly in the new compiler implementation (which can lead to faster compilation, but is unrelated to the performance of the compiler output).

But Zig might have some features which might theoretically allow the compiler to generate slightly more performant code (compared to portable standard C code). E.g. since function arguments are read only, the compiler might be able to avoid a copy on the stack when passing structs > 16 bytes by value.

Also since Zig programs are (AFAIK) generally compiled as a unity build, the compiler has more inlining opportunities compared to a conventionally linked C program (without LTO).

Etc... etc.. so theoretically it might be possible, but I'd still take such statements with a grain of salt until demonstrated on real world code.


See https://m.youtube.com/watch?t=3566&v=gn3YsZ6HUHw Transcript with some omission.

> Andrew: I claim that Zig is faster than C....

> I can give micro benchmarks of messing around with integers...

> I would make argument that based on principles of the language, the conventions that we have, the organization of standard library, there are also results in practice that Zig application tend to be faster...

> In C signed integer addition overflow is undefined and unsigned integer overflow is wrapping. In Zig it's consistent and signed and unsigned overflow is illegal behavior.


Andrew argues that zig can produce faster programs than c or rust towards the end of the video: https://youtu.be/gn3YsZ6HUHw?t=3566


The article is quite weak IMO and mixes up a few things. For example, there isn't really a "global allocator" built into the C language. In the case of your quote, this is a misunderstanding on the performance optimizations that were done on the Zig compiler itself (it compiles faster). But that makes no difference to the performance of the compiled program. And the techniques that were mentioned can be used in the implementation of any other compiler as well.


In this case, I think this is poorly written. Andrew was talking about the Zig compiler and the data-oriented approach he used, leading to better compile speed.

About the speed of the generated code, I have not seen many benchmarks, but I expect to see minimal differences with C.

With better semantics around pointer aliasing and parallel-friendly loops they could produce faster machine code than C, but likely not by a huge margin.


C performance is no great shakes anymore. C++ and Rust routinely exceed it. Java and Javascript, even, sometimes.

People still compare to C because (1) it is easier to beat, (2) you are less likely to be accused of cooking the comparison by coding the reference badly, and (3) you don't provoke the question, "why not just code in that, then?"


The first two paragraphs make reading further superfluous.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: