The Dynamic Runtime Inlining project
github.com
github.com
There's obviously been a lot of research on JIT compilation for Java and JavaScript (hence why they can run very fast, especially JS, despite being very dynamic languages). But what if we apply JIT techniques to already-AOT programs? We lose performance consistency (although we can keep the lower bound of the base program), but we get even faster performance, and it makes debuggers' and hot-code-reloaders' jobs easier because we already have the ability to compile and recompile code during execution.
How come?
[1] https://blog.chromium.org/2020/08/chrome-just-got-faster-wit...
Complexity:
- JIT is only faster if it often enough recognizes the right things to JIT in the right way.
- At the same time the JIT compilers share resources with the running program limiting how complex the analytics and optimizations it can do can become. This is not just a problem wrt. to hurting the main applications performance due to reducing available resources but also e.g. matters for energy usage.
- Worse there is often real time time bound on how long a JIT compiler can take no matter how much resources it uses or responsiveness will suffer to much.
- Similar if no additional tricks are used (which often add painful complexity) the optimizations have to be done every time the program is started again and again, hurting auto scaling or ephemeral programs.
- There is also the problem that workload/data specific optimizations (which gives JIT the additional edge) rely on some data/workloads always having a specific format. If this invariants change they might become slow or might even to fallback to other code (which implies the invariant also needed to be checked). One exception is if you hint to the compiler that certain invariants (e.g. function pointers) won't change (like in the OP article).
Predictability:
- the performance of AOT focused language code is often more predictable making it often easier to optimize
- in JIT languages you rely more on the compiler being able to detect the right patterns, this isn't always easy and even with AOT languages this sometimes isn't easy even through the compiler can do much more work to analyze the context
compiler-existance:
- Many AOT language can take advantage of LLVM/GCC so a lot of work has gone into optimizing them before they even started.
- And while you can use LLVM to JIT it is often not viable as it's a rather large dependency with a lot of different versions which itself is optimized for throughput and not latency and is also tuned for a use-case which is allowed to take a while and consume quite a bit of resources.
Language Design:
- Features AOT languages are often designed with their AOT nature in mind, many JIT/Interpreted languages are not. Sometimes they outright have features which can impede JIT optimizations or at least make them more complicated/harder to find/less fast.
- If you throw enough money at a badly for JIT designed language you probably still get good results, e.g. see JavaScript. But this isn't a story of success, it's a story of dynamic/politics leading to an absurd wast of human resources (dev hours) across many companies burning millions if not trillions over the years to work around language limitations.
So any hypothetical language perfectly taking advantage to JIT to become "the fastest" would probably be much closer to a classical AOT language then e.g. JavaScript (as in less dynamic more strictly structured, maybe GC but maybe not or at least more restricted to reduce GC abuse possibilities, no or very limited runtime reflection, etc.).
To top this of some of the benefits of JIT can be brought to AOT languages. For example you can compile a binary with LLVM in a way so that it collects statistics which then can be used to fine tune optimizations (better decide if a path is cold or which loops to unroll how much etc.). Drawback is setting it up in you deployment can add quite a bit of complexity. Another example are tools JIT some very few predetermined points, like de-visualizing/inlining virtual function calls or for one-time initialized global remove the "is initialized checks".
Which means at the cost of more complex deployments doing some profiling to find some problematic vcalls and similar you can get many (but not all) the optimizations JIT languages have while having most of the benefits of AOT language. Through it only makes sense if your company/product reached a certain non-small size. Or when there is more modern tooling around to make such things easier to setup.
Runtime profiling has a cost, and the benefit has to outweigh that cost. Collecting receiver type profiles in order to speculatively inline has a big payoff in, e.g. Java or JavaScript, but probably not C++ or Rust, where programs have less polymorphism because programmers routinely avoid polymorphism in hot parts of the code.
And, to my mind at least, there is something interesting/innovative about seeing these ideas applied to C++.
EDIT: Can't tell the difference between ~ and - apparently
DRTI is pronounced "dirty." This sounds impressive as heck but also I'd never be an early adopter.
Inlining is the ultimate optimization, but only as a foundation for others: Modern CPUs have rendered function calls (even predictable virtual ones) nearly free.
For the C calling convention -O3 is good enough. With this you need to explicitly mark them, similar to [[always_inline]], just in a separate text file. This hardcodes r14, so it would be only useful for x86_64.
Also inlining just enables further optimizations, previously missed. something like qbe is much more interesting.
This thing seems to generate code to check if the type matches what was inlined, and do the ordinary virtual call if it doesn't match. Virtual-call branches are well predicted nowadays, but the opportunity to integrate the code before and after the call might be worth something, probably by deleting code that computes something thrown away in that case.
Yeah GCC and clang will do what you described in some cases, they will insert a check and try to make a non-virtual call when possible. But also as you mentioned, they real advantage is from the downstream optimizations that unlocks, which they cannot do.
JVMs typically use dynamic class hierarchy analysis that doesn't require runtime profiling, but can require deoptimization on class loading.
I could see this getting more play than PGO if it becomes really easy to turn on.