logoalt Hacker News

thefauxlast Tuesday at 6:22 PM5 repliesview on HN

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.


Replies

dzaimalast Tuesday at 6:39 PM

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.

mluggyesterday at 8:55 AM

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.

[0]: https://kristoff.it/blog/zig-new-async-io/

flohofwoeyesterday at 8:44 AM

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 ;)

mluggyesterday at 8:45 AM

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.

chaz72last Tuesday at 6:39 PM

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.)