Do we need a link step?
robert.ocallahan.org
robert.ocallahan.org
Linking was designed when computers literally didn't have enough memory to load the IR of an entire program into memory at once in order to compile it, so programs had to be split into many files and compiled one file at a time. Primarily for C, but as we know, its very naive file-inclusion mechanism now results in huge translation units anyway (even worse in C++). It's antiquated and highly inefficient (O(n^2)).
Just for reference, the Virgil compiler, right now the largest Virgil program in existence, is about 43KLOC. It takes about 200MB of memory to parse, verify, transform, optimize, and generate code for it, and about 300 milliseconds of execution time, on one thread. Whole program compilation won't scale to millions of lines, perhaps, but hundreds of thousands are clearly within reach. And I'm OK with that.
Another point of linking is, well, linking together different source languages, often a high-level language and and assembly, or a high-level language and C. In certain areas, libraries written in Fortran are important, too.
A complete language monoculture often is not attainable, even with languages as total as C++, Rust, or Java.
> Turbo Pascal's built-in linker automatically removes unused code and data when building an .EXE file.
Turbo Pascal 6.0 Programmers' Guide page 258.
This is just plain wrong. Linking was, and still is, about assembling a final executable in a format and a way that accurately describes the data within it. For example, kernels tend to need very customized linker scripts in order to place all of the data in the correct spots.
The reason why linking can be hidden in most cases is because building an executable for a specific system is generally straightforward and well specified, needing little to no tweaking or opinions about how it's built. This is not always the case, and linking was certainly not "invented" to work around binaries with no sources...
The case of VM-based languages is entirely out of purview of the system linker, too.
This isn't theoretical, it's exactly how Virgil generates binaries for x86-darwin, x86-linux, and x86-64-linux. The output is just one binary and no C environment. No C libraries for anything. Instead, the Virgil compiler knows the kernel ABI for the target and library code (written in Virgil) implements a thin portable layer directly in terms of syscalls using raw pointers.
I have worked on projects where a single source file is as large as that, before the pre processor has had a go at expanding includes.
[1] https://raw.githubusercontent.com/mackron/miniaudio/master/m...
Holy moley, yuck. It would truly suck to have a project that used that header in more than one file!
The only real loss of only referencing audio-library-specific types in 1 translation unit is that the class you write to wrap audio-library-specific types must be located behind a pointer (either forward-declare class LibraryType and embed unique_ptr<AudioLibraryType>, or put AudioLibraryType or unique_ptr<AudioLibraryType> in a TU-local subclass of a public interface).
Meanwhile Rust lacks the concept of translation units (instead each crate is a single rustc invocation which may sometimes be split into codegen units), and hides the presence/absence and location of compilation firewalls from the user. I don't actually know how to trigger or avoid cascading recompilations when using Rust's cpal audio library.
Not currently, no. I've thought about trying a completely transparent behind-the-scenes incremental compilation scheme, basically hiding customly-formatted object-file equivalents somewhere on disk (and/or in memory), checking source file modification times, and stitching them together like a linker would, but I did the cost/benefit analysis and since I am generally working on programs < 50KLOC in size, it's not a problem I have yet. (Relatedly, in my bug/debug/rebug cycle, aka development, I often run my programs in the compiler's built-in interpreter, which only parses and verifies the code, and then interprets IR directly, saving 90% of compilation time).
As for linking with other languages, I think it's considerably easier to have the Virgil compiler generate a ".o" [1] that is relocatable (i.e. linkable) and describe the imports/exports of a Virgil module in the Virgil language [2]. Then this ".o" could be linked in with the existing C/C++ ecosystem, rather than me trying to replicate linking in my compiler. The Virgil compiler really likes being able to nail down static (virtual) addresses; I suppose it can keep doing most of that if it doesn't have to link with too many other object files that constraint its placement choices.
[1] The Virgil compiler can generate either ELF32, ELF64, or MachO binaries, so it's incremental work to set a few flags on sections and work out how to emit relocation entries.
[2] I have a prototype of this for the Wasm target, so that Virgil programs can import unknown functions from modules, and the compiler generates a Wasm module with appropriate imports.
[1] well, except for personal projects, but that's not work.
KDE 57.9M LOC, 19402 years of effort
NetBSD 50M LOC, 16822 years of effort
Linux kernel 36.6M LOC, 12,369 years of effort
Google Chrome 25M LOC, 8131 years of effort
Mozilla Firefox 20.5M LOC, 6564 years of effort
FreeBSD 17.2 LOC, 5538 years of effort
GNOME 15.7 LOC, 4946 years of effort
LibreOffice 9.5M LOC, 2907 years of effort
QT 5, 8.2M LOC, 2525 years of effort
OpenJDK 8.4M LOC, 2583 years of effort
IntelliJ IDEA community 6.1 LOC, 1850 years of effort
GNU compiler collections 7.5M LOC, 2,327 years of effort
Blender 3D 2.1M LOC, 612 years of effort
PostgreSQL 2.0M LOC, 596 years of effort
TensorFlow 2.5M LOC, 724 years of effort
And that is just counting open source/free software. It's not hard to imagine there is plenty of projects out there that have existed for more than 10 years with more than 10M LOC.- [1] https://www.quora.com/What-are-some-open-source-projects-wit...
Linking in C/C++ is pretty efficient in that it is mostly about loading symbol tables and then byte-copying sections together, and then applying relocations. The non-linear part of C/C++ is mostly all in the front end when it must reparse the same header files dozens or hundreds of times over, each time building an AST, verifying the AST, rebuilding the IR, and even repeatedly generating code for the same functions/classes. The linker generally deduplicates functions, sometimes dangerously, like taking the first definition of a given symbol it sees.
[1] Of course, the entire compilation process does involve visiting and transforming the IR in multiple different ways in different passes. The IR is also indexed by different keys, e.g. by class or by method or by type or by signature, so not all passes visit the entire IR. And of course the optimizer might decide to run different passes on a given method based on its characteristics, like whether it has a loop or is very branchy, etc.
Still true of C++
I think we need more linkers, and more thoughts like this on why that step is still necessary and how we could do it better. Look at the "all static" docker images and how the Lua FFI was "faster than C"; these are effects of linking and shared libraries being Deep Magic that no one wants to think about; thus we get efforts to work around the problem.
Maybe every executable should do its own final assembly of memory image and fixups and stuff at execution? Maybe we should be executing memory dumps that include zero'd pages already... maybe we need one or the other depending on the circumstances, and no "one size fits" all solution is suitable.
Sounds a lot like dynamic linking, though that does have an extra step of indirection that can result in performance issues, either directly due to the pointer hopping or indirectly due to the inability to inline a dynamic function.
> Maybe we should be executing memory dumps that include zero'd pages already
For global variables, C/C++ include this already. That's why the initialization of `int x;` depends on whether it's a global variable or not. For newly allocated memory, whether on the stack or on the heap, initializing to a default value that is later overwritten would have a runtime cost. (e.g. If it is immediately passed as an out parameter to a function.) However, for global memory the initial value of `int x;` can be set in the program image with no runtime cost, so it is done automatically.
Except, the size of the object file will inevitably change. So you are stuck reimplementing a mini-filesystem with logic for allocating/freeing blocks within your final executable, or loading the entire old version in memory, and then producing the new one from scratch. Or, you just keep things in separate files, let the OS handle it for you and go sip some champagne on a New Year's Eve...
Really, C/C++ build logic sucks in many ways, but adding an extra level of complexity to the linking process won't make it better. If you want something cool, check out C++20 modules [0] instead. They will make compilation dramatically faster when used properly.
It would be more like malloc/free than a mini-filesystem.
I don't think this is likely to be a big problem because the sizes of object files will often not change very much, we know how to implement malloc/free with reasonably low fragmentation, and it doesn't matter much if there is some wasted space in the final executable, when the goal is "we're going to build an executable that runs "pretty fast", for testing purposes".
What it does is bundling kernel object files into an archive, which is linked at boot time to form the final kernel image. Note that the link order is randomized each time the kernel boots, since KALR is a security vulnerability mitigation designed to thwart exploitation attempts.
If an executable is desired, you just dump the contents of memory to a file.
Some implementations can't dump memory: ABCL (compiles to JVM), ECL (compiles to C), ...
Sorta like the trampolines linker will insert if it can't reach the code.
Symbol resolution has lots of complicated rules. But I suppose if we are designing a new system we aren't bound to those.
I would suggest that, relative to this new link-less compilation, the baseline link behavior should be considered like a static optimization that trades off static build time against code size and execution performance.
This is how ELF shared object external references get resolved.
> Update Zig can do something like this.
The mechanism zig uses is indeed leveraging the GOT.
If an object file contains calls to other object files, they get linked during load time, linking is not considered as a separate step.
Note that other platforms couldn't have it, they just can't decide on whole platform development experience.
On Oberon, it is a bit sad that most compilers that were developed outside of the project focused on static linking without that development experience.
This sounds unnecessarily complicated