This is a very good question with a somewhat involved answer. Bear with me.
This is exactly how Julia’s precompilation works. What is referred to as precompilation happens when packages are updated (or upon first execution when the combination of versions you are using has changed for some other reason—this is uncommon but can happen). However, precompilation doesn’t save jitted machine code… Except on Julia master this branch, where @tim.holy recently implemented saving jitted machine code to precompile files (.ji
files as they’re called). However, I’m pretty sure Julia’s precompilation is not what you’re thinking of because…
For compiled languages like C++, you generally use external libraries that you download in a format that is already compiled.
This describes what is called “separate compilation”, which is when separate source files can be compiled independently and then “linked” together to form an executable. The linking step is somewhat costly but not nearly as expensive as the individual compilation work. And yes, separate compilation is a feature of most statically compiled languages like Java, C, C++… but not of others like C++.
Wait, how can separate compilation be both a feature and not a feature of C++? Because C++ has various different styles of usage. If you avoid template metaprogramming then C++ supports fully separate compilation. But there are many C++ programmers who make constant and heavy uses of the Standard Template Library (STL) and other template libraries based on it. And this is basically a completely different language—in many ways a more expressive one. But also one which doesn’t really support separate compilation. This is why Google’s C++ style guide forbids templates in their code: templates would make compiling their monorepo take forever because it breaks separate compilation.
Julia is very similar to C++ with heavy use of templates: it specializes generic code very aggressively on the actual runtime types that get passed to that code. You don’t have to write explicit templates for this to happen—it’s just done automatically. If you define MyType
with a custom isless
method and then call sort
on Vector{MyType}
, Julia generates new machine code for sort
that is specialized for Vector{MyType}
and which probably inlines the implementation of isless
into the sort body. This kind of specialization is crucial for optimal performance. And this is exactly what STL’s std::sort
does too. On the other hand, this is not how libc’s qsort
works. Instead qsort
achieves genericness by working with void *
pointers and arbitrary array strides and using a function pointer callback for comparisons. As a consequence, qsort
is slooow, whereas std::sort
is fast. All of Julia is automatically like std::sort
. (Many static languages which support generic programming and separate compilation, on the other hand, are automatically like qsort
instead—OCaml, Haskel, etc.) This is one of the major reasons why Julia is so fast—great speed is one of the major advantages of this approach. But one of the major disadvantages is that it makes separate compilation very challenging.
That’s an idea, but the “even if supercompile took 3 days” part isn’t hypothetical, it’s actually an underestimate of how long it would take to compile every possible specialization. The number of possible specializations of f(a::A, b::B)
is N(A) * N(B)
where N(T)
denotes the number of concrete implementations of T
. The number of specializations of f(a::A, b::B, c::C)
is N(A) * N(B) * N(C)
. And so on—it’s exponential in the number of arguments and multilinear in the number of concrete implementations of every argument type. Worse still, it is trivial to write a parametric type which has an infinite number of concrete implementations, meaning that the number of possible specializations of f
would be infinite. So when I said underestimate above I meant that supercompilation would never stop.
The same thing is true in C++ with templates, since it’s easy to write a generic type that has an infinite number of potential concrete instantiations—that’s kind of the whole point of generic types! So what do they do? They compile the whole program so that they know what actual instantiations of templates are used: templates are fully expanded even before the compiler proper starts compiling. Julia doesn’t require you to do that, of course, and instead generates only the specializations that are actually used via JIT. Moreover features like eval
and include
mean that one can express Julia programs for which it’s inherently impossible to say what types will even be instantiated and what specialized methods will be called. What if you want to do what C++ does? Well, that’s exactly what PackageCompiler and StaticCompiler do. You still need to keep the JIT around just in case (because of eval
), but you can try hard to generate all the code you think you’ll need in advance.
An obvious followup observation is that even for C++ with templates there are often completely separate libraries that can be used without needing to recompile everything. Can’t Julia do something like that? Indeed, that is possible and @jeff.bezanson has gradually been working towards it. We’ll be able to generate completely separate libraries written in Julia at some point in the somewhat near future. With further work it will even be possible for those separate libraries to not depend on LLVM or on the garbage collector (either because it can be proven that they don’t need them or by just letting them fail if they do try to use either).
To wrap it up with a summary:
- Julia precompilation already works like that
- But it doesn’t include machine code
- Except on Julia master it
doessoon will - But that still isn’t “separate compilation”
- Which is available in most static languages
- But not in C++ with templates
- Yet even there some separate compilation is possible
- And that also is coming to Julia