Compiling to native code is much less forgiving -- security-wise -- than interpreting. A stray read or write can very easily lead to an exploit.
As a result, compiling "foreign" code to native code on the fly -- whether via AOT or JIT compilation -- is fraught with peril. All mainstream JS engines have had CVEs filed against them, for example.
JIT (and AOT) compilers are therefore at a clear security disadvantage compared to interpreters when it comes to running "foreign" code.
Size & Complexity
In general, a pure interpreter is (much) smaller than a JIT compiler, and thus much less complex. This ties in with the above security issue: the simpler, the easier to secure.
In general, a pure interpreter is more portable than a JIT compiler. A JIT compiler must be specialized for everything target platform (both architecture and OS), whereas a pure interpreter can rely on existing toolchains to be compiled for the right platform.
Conversely, a statically compiled binary is stand-alone and can be delivered on its own to any user, whereas for interpreted or JIT compiled language, the user typically must have both the "program" (in some form) and an interpreter or JIT compiler of the right version. Bundlers do exist, but the resulting applications are even bigger than statically compiled applications.
So called dynamic languages -- ie, languages where the type of a value is not known at compile-time -- are a hard target for AOT compilers, so that in general there are only interpreters or JIT for them.
In such a situation, there's typically a start-up time vs run-time trade-off:
- An interpreter starts up near immediately but may run slower.
- A JIT compiler starts up slower -- compiling everything it comes across -- but will run faster in the long term.
The above trade-off can be alleviated (somewhat) by using tiered compilation, which is often used in JS engines, or the C# or Java runtimes. Tiered compilation is the idea of starting with the fastest start-up method, and over time to compile more and more optimized JIT code.
While in theory a JIT compiler should be able to outperform anything, in practice it rarely plays out and a JIT compiler typically ends up producing a program which runs slower1 than one produced by an AOT compiler, due to a lower optimization budget.
There a specific exceptions: some workloads benefit from a run-time specialized inner loop.
The difference between theory and practice is perhaps not too surprising: in order for a JIT compiled program to outperform an AOT compiled program, it must take advantage of run-time information. This, in turn, means that a JIT runtime must not only JIT compile a program, but also instrument it to gather the necessary information, and guard it, in case the information it optimized for falls through.
This has 3 downsides:
- Information gathering takes up run-time, run-time that could be put to use to actually run the program, which the JIT compiler must then make up for, somehow.
- Hot-patching -- used both for de-optimizing and splicing in a more optimized function -- requires to leave some "scaffolding" in place. Scaffolding which takes up run-time, and which once again the JIT compiler has to make up for, somehow.
- Guards -- used for de-optimizing -- which are extra run-time checks, which the JIT compiler has to make up for, somehow.
And of course, the very run-time used up to JIT compile also has to be made up for.
All in all, this means that JIT compiler start with a penalty2, compared to AOT compilers, and only if they uncover critical run-time information unlocking worthy optimizations will they able to produce a program which will outperform an AOT program.
And of course, very optimized AOT programs also use run-time information gathering for Profile Guided Optimizations, except ahead of time3 so they don't pay for it at run-time.
1 JIT compiled pre-optimized WASM produced by AOT compilers for C, C++, Rust, or Zig, for example, is routinely 2x slower than the natively compiled program. There's indubitably some overhead from the sandboxing applied, but still...
2 A penalty on top of the hand tied behind their back, that is, as they typically have a much stricter "optimization budget" than AOT compilers, since they're doing the optimization work while the program is running...
3 PGO ahead of time may suffer from artifacts effects, yes. But then again, the latest C# JIT will splice in a non-instrumented optimized version, so that should the workload change after optimization, it also uses the wrong profile. Why? Because keeping the instrumentation in would negatively affect performance...