Hacker News new | ask | show | jobs
by eigenspace 32 days ago
Julia actually has some really cool machinery for handling this that I would encourage other JIT languages to copy.

Whenever you call a function, that function and any calls in that call stack occur in a 'fixed world age'. Within a given world-age, method tables and global constants are all fixed, and the langauge can be analyzed like it's statically typed (there are escape hatches like `invoke_in_world`, and `invokelatest`)

Between world-ages, things are allowed to change. When a function calls another function, we add a 'backedge' from the caller to the callee.

So if I have `f(x) = g(h(x))`, and I redefine `h`, we then say it's no longer valid, and then we look at the backedge that leads from `h` to `g` and say the old definition of `g` is also no longer valid, and then we go from `g` to `f` and also invalidate the old definition of `f`.

This means that once `f` is called in a new world age (the world-age gets incremented every time a new method is (re)defined, or if a global const is changed / defined), the compiler knows that it has to recompile `f`, `g`, and `h`. What's especially cool is that this system works regardless of inlining, and it allows us to safely do all sorts of interproceedural optimizations, but in a JIT compiled language.

1 comments

That is very cool indeed. Are there limitations that this imposes? Is Julia a whole world compiler or does it support partial compilation?
There's two main limitations:

1. If you try and re-define a global constant or add new methods inside a running program using `eval` or whatever, then your running program won't see those changes until it advances the world-age (i.e. either by using `invokelatest`, or by returning to the top-level scope). Note though that things like closures and defining functions within functions is fine, you just can't do an arbitrary `eval` to define something completely dynamially

2. Method invalidations can cause a lot of compilation latency. If you load a package that invalidates a bunch of already compiled methods, then those methods will later need to be recompiled, which means you hit some more compiler latency than expected. These invalidations can have false postives too, so sometimes more methods get invalidated than you'd want

__________________________

> Is Julia a whole world compiler or does it support partial compilation?

On the LLVM side, we only do partial compilation. Every function method specialization in each different world (modulo inlining) is its own LLVM module that gets compiled in parallel by LLVM. Non-inlined function calls then involve linking these modules.

On the julia-side with our own custom internal IRs though, that's where we perform whole-world style interproceedural optimizations and inlining before handing the individual compilation units to LLVM. At least if I'm using "whole world" right here. What I mean is essentially everything statically known to be reachable from a compilation unit's entry-point given its signature. If by "whole world" you mean compiling every possible method signature, that's not possible in julia at all, because the space of possible method specializations is infinite due to parametric types.

We generally get the best of both worlds with these two approaches (at the cost of just using a lot of space to store all the different possible specializations and all of our differnt IRs and different pieces of machinery).

That is really interesting, and very clever - thanks for the details! What you describe is indeed what I meant by whole world, I'm not sure if there's a better term of art for it.