Join the discussion
Write your take first — we'll ask for email only when you're ready to publish.
- Hacker News
- I just looked up a Hello World program from the Zig Wikipedia article:
That's a lot to follow, just to output a plan-text message, especially after this line: "The primary goal of Zig is to be a better solution to the sorts of tasks that are currently solved with C. A primary concern in that respect is readability…"const std = @import("std"); const File = std.Io.File; pub fn main(init: std.process.Init) !void { _ = try File.stdout().writeStreamingAll(init.io, "Hello, World!\n"); }by dlcarrier - That's the 'official' hello world which is indeed a bit verbose (it's "correct" in the way that it generally shows how to stream formatted text to stdout though).
Arguably this is the more beginner friendly version, this prints to stderr though:
hello.zig:
...and thenconst print = @import("std").debug.print; pub fn main() void { print("Hello World!\n", .{}); }zig run hello.zigby flohofwoe - Whats the point of evaluating technology from hello world programs?by tester756
- The issue is that most "hello world" programs are not correct.by defen
- Everything it's doing it clear and readable. It's just not as easy to write. It streams the bytes to stdout using the default IO interface, and it can fail.
Alternatively, here's a simpler version (prints to stderr).
In practice, you normally don't want to print messages to stdout. So the increased friction here actually pushes you in a better direction.const std = @import("std"); pub fn main() void { std.debug.print("Hello, world!\n", .{}); } - I don’t think you can judge a programming language based on its “Hello World.” AppleSoft BASIC has this:
10 PRINT “Hello World”
Beautifully simple and readable. But it’s not a good language by modern standards.
In your example, I see a lot of complexity being surfaced: output streams, locals instead of globals, error handling. I don’t know Zig but all of those are things that are important to address, and I like that the example doesn’t sweep them under the rug in pursuit of a false readability.
by jdlshore - It disappoints me how unseriously the industry has taken compilation speed for so long. I'm glad to see Zig doing incredibly valuable, high-impact work here.
- Yes, we get people amazed to rediscover Modula-2, Object Pascal compiler execution speed.by pjmlp
- Golang's reason for existence is pretty much compilation speed.by logicchains
- Well, I remember back in the day Java was winning a lot of love from C++ devs over its much-much faster compilation than C++. It was important, even 15+ years ago. People praised faster iteration time while coding.by deepsun
- Does this work for release builds or just debug builds now?by remywang
- I don't think it will work with llvm/release yet but it might some day maybe.
Getting incremental linker to work with llvm is kinda hard atm. Maybe the zig team has a plan dunno.
by minraws - It only works with our self-hosted code generation backends (the main one being for x86_64), which right now don't have any optimisation passes. It's planned that they will in future, but that's a long-term goal. Also, any optimisations which propagate information between functions (the most obvious and important one is inlining) are more-or-less incompatible with incremental compilation (I touch on this in the post IIRC), so once this does work it'll probably still be limited to a subset of optimisations.
TL;DR: only debug builds for now, could extend to release builds once we have our own optimisation passes one day, but some optimisations will still be inapplicable.
by mlugg - The zig compiler can compile C so will it work with C also?by hoppp
- not quite! if your project is mixed C/Zig, then editing Zig would work but editing C would not. the Zig compiler caches Zig AIR but no information from C. plus, the C compilation can only really be done with LLVM and writing a self-hosted backend would be PITA for Cby bpavuk
- I've always thought this was fascinating, but the only incremental compilation I knew was obscure programming languages and Rust. Oh yeah, I guess Roslyn?
Really fun and fascinating problem to work on.
by sigbottle - Wasnt Roslyn 1st at implementing this on such scale?by tester756
- > Dependencies on the body of a runtime function are impossible (at least in the simplified view I’m presenting here)
How does this work given that e.g. a constant can be computed by a comptime function?
by patrec - It works through the fact that I specified "runtime" function ;)
A bit after that quote I have a note about `inline` functions in Zig, where I mention that they perform semantic inlining, which means dependencies triggered by the function actually get associated with the call site. Well, `comptime` function calls work just the same way---in fact, to the compiler, `comptime` calls are almost exactly identical to `inline` calls. So when we encounter a comptime function call, we start analyzing the ZIR for that function's body, but we don't switch our analysis unit, so comptime stuff doesn't really complicate the dependency graph at all (aside from the fact that it means you can depend on any number of source code hashes, instead of everything depending on exactly one).
With all that being said, there actually is a (completely unrelated) way in Zig to can depend on the body of a runtime function (hence why the quote includes "at least in the simplified view I'm presenting here"). It's to do with "inferred error sets" (IESes for short). If a function's return type is written `!T`, that means it can return an error, but we're asking the compiler to figure out exactly which errors are possible. So if at some point we need to know that set of errors (e.g. because the user has done some reflection to try and access the list of errors), that's where we get a dependency on a runtime function body, because we need to analyze the function body to learn about all the places it might return an error.
by mlugg - I'm becoming a big fan of Zig every since learning about `zig cc` as a way of dipping my toes in it. I was already impressed by the build caching so I'm keen to play with thisby anitil
- There is something that I don't fully understand about this design: why are they insisting on building a giant binary for debug builds that contains all of the code? From my perspective, a simpler approach is to generate many smaller shared libraries (perhaps at the file level) and link them in to the final binary. With this approach, the program binary would have a tiny text section and a (potentially long) list of shared libraries to load. But even with thousands of shared libraries to load, the resulting program binary would not be all that long and there would be no need for binary patching.
I understand that for a release mode a single giant binary may be desirable, but I am struggling to understand this design for debug builds. Moreover, while reading this article, I found myself wondering what happens if the main binary becomes corrupted. Maybe the user cancels compilation with ctrl+c while it is patching the binary. Even if they have a story for avoiding and/or detecting corruption, it is simpler to not patch in the first place and always generate a new main binary. Again, this is reasonable because the new binary is mainly just a list of shared libraries to link which will not take up much space and can be written to disk quickly. Moreover, this process can be done recursively, e.g. at the subdirectory level, so that during incremental linking a few quite small shared libraries may be produced rather than patching in the new code and writing cascading relocations.
by thefaux - If you choose to build static or dynamic libraries, I expect you’d still get to do that, and I expect as long as those don’t change and only the main binary needs patching it’d work the same. (Though I’m waiting for the next tagged release to really try it out myself, so that’s not like a guarantee or anything.)by chaz72
- Zig uses a "single compilation unit" compilation model, so even if we did what you're suggesting, pretty much everything in the post would still be relevant, especially the whole-file stuff and the semantic analysis stuff. Note that there's not really anything special about putting things in separate source files in Zig---files are just a unit of code organization, like namespaces. They don't help the compiler at all (aside from in that very first part of compilation, but that's fast regardless).
The only thing I discussed that we could potentially avoid using your suggested strategy is incremental linking, but AFAICT that would only be easily avoidable if we made a separate shared object for each individual function. I guess we could group them arbitrarily and then re-run codegen for everything in one shared library when the other parts change, but that frankly doesn't sound much less awkward than incremental linking!
But putting all that aside: the only thing this approach would really achieve is offloading the linking work from the static linker to the dynamic linker (aka runtime linker). So if it did make compilation faster, I'd expect all of the saved time to just become runtime execution time---which is kind of worse, because the average number of times you run a compiled program is probably >1!
Of course, there's also the obvious point that dynamic linking only works in cases where you can run dynamic executables. For instance, that approach wouldn't work when doing operating system development, while our approach should work completely fine for that use case.
by mlugg - IME dynamic libraries ever only add more problems on the pile but never solve anything (they're basically only good for plugin systems, or as userland operating system interface).
Shared libraries work slightly differently on each operating system, and at least on Windows and macOS, having to load thousands of tiny shared libraries will almost definitely cause your startup time to explode. I once wrote an 'OOP system for C' where each class lives in its own DLL, and let's just say that this was probably the most stupid idea I ever came up with, and I came up with a lot of stupid ideas in my life ;)
by flohofwoe - Oh, I forgot to respond to the "corrupted binary" thing in my other reply, sorry.
Right now, yep, corrupting the binary that the compiler reads would crash the compiler. In future, we want to detect the corruption and force a clean build. Note however that the Zig compiler is writing the binary to its internal cache directory (typically `.zig-cache/`), and the build system then copies the final artifact to your output ("prefix") directory (`zig-out/` by default), so it doesn't matter if the user messes with the final binary in `zig-out/bin/my_program`, because that's just a copy. Lastly, this is not implemented yet, but we will definitely make sure that Ctrl+C leave the cache in a clean state. I'm pretty sure our incremental compilation system has a nice property that you can just cancel an update partway through and continue it later without too much effort. See also Zig's IO interface [0] for information about cancelation. The compiler should already support graceful-ish cancelation internally (I won't claim to have verified this, because we never actually do cancel compilation right now, but I'm not aware of any glaring issues!). I don't think it'd be a crazy amount of work to improve that so that it also leaves incremental compilation in a valid state.
by mlugg - A quick test (C, clang) gives me that a binary depending on 1000 shared libraries, each containing a single function returning an integer, with a main function summing up the results of all those functions, takes ~270ms to run from the dynamic linker overhead. So you'd definitely want a good chunk below thousands.
(a process with 100 shared libraries takes 6ms to run, which is a lot better (0.9ms for 1 library, for reference), but, especially with in-place patching skipping work on unchanged values, static linking still has a good shot at beating dynamic linking, especially if you run the binary multiple times)
Incremental compilation generally already depends on its stored intermediate data not getting corrupted, and the final binary need not be any differently handled in that aspect.
by dzaima - This post is really interesting. As a member of the rust-analyzer team, I cannot avoid comparing it to the situation in Rust land. Rust famously has not less (or even more) sophisticated system for incremental compilation, yet its compilation is way slower. I attribute that to two main things:
- Language design. Zig was designed for fast and incremental compilation, Rust is just not. For instance, the post states that Zig has four properties (layout, type, value, body) that the compiler has to track for changes. Rust has much more, to the point that tracking them statically is just impossible, so the compiler uses a query system that tracks them dynamically, which adds overhead.
- Compiler implementation. Rust is much more complicated to compile than Zig, and rustc is both older and bigger (10x-20x LOC) than the Zig compiler, making changing it way harder.
by afdbcreid - Isn't most of Rust's compilation overhead from the llvm backend?by lsuresh
- Zig's toolchain work is continually impressive. While I still don't plan to write software in it, given that I believe memory safety is table stakes, all of this stuff is very, very good. Before the incremental work, it was the toolchain and cross-compiler work. The toolchain stuff has continually been fantastic. I'm very curious to see what they come up with next!
> Semantic analysis is the most difficult part of the compiler to handle incrementally. Perhaps unsurprisingly then, this is where language design starts to matter a lot: while I am pretty confident that most modern languages could support incremental compilation similar to how we do, certain design decisions can make that much more difficult. Zig has had its design tweaked over the years (sometimes controversially) specifically so that it is easier to support fast incremental compilation.
This is something I wish that we had done with Rust. It is impossible to do all of the things at once, though, and we already had a tremendous amount of things to do. This is also part of the "when do you ship 1.0" tradeoff; for our goals with the language, 2015 was the right moment to launch, but if had a few more years to bake things, maybe we could have made compile times way faster. Software engineering is hard.
by steveklabnik - The thing with rust is that you get safety with slow compilation, it's a tradeoff.
Zig doesn't have the same safety guarantees, it's on the dev to use safe coding patterns, so the tradeoff for safety is discipline or experience.
by hoppp