Is JIT a compiler or an interpreter

Understand the differences: traditional interpreter, JIT compiler, JIT interpreter, and AOT compiler

overview

A interpreter for the language X is a program (or a machine, or just some kind of mechanism in general) that does everything in the language X written program p so that it performs the effects and the results according to the specification of X evaluates. CPUs are usually interpreters for their respective instruction sets, although modern high-performance workstation CPUs are actually more complex. You can actually have an underlying proprietary private directive set and either translate (compile) or interpret the externally visible public directive set.

A Compiler of X to Y is a program (or a machine or just some kind of mechanism in general) that any program p from one language X into a semantically equivalent program p ' in one language Y translates, so that the semantics of the program are preserved, that is, the interpretation p ' for using an interpreter Y provide the same results and have the same effects as the interpretation p with an interpreter for X . (Note that X and Y can be the same language.)

The terms AOT (ahead of time) and Just-in-Time (JIT) refer to the Time of Compilation: The "time" mentioned in these terms is "runtime", ie a JIT compiler compiles the program in such a way that as it is executed , an AOT compiler creates the program, before running it becomes . Note that a JIT compiler by language X to language Y somehow with an interpreter for language Y must work togetherOtherwise there would be no way to run the program. (For example, a JIT compiler that compiles JavaScript to x86 machine code does not make sense without an x86 CPU. It compiles the program while it is running, but without the x86 CPU the program would not run.)

Note that this distinction makes no sense for interpreters: an interpreter executes the program. The idea of ​​an AOT interpreter that executes a program before it is executed or a JIT interpreter that executes a program while it is executing is nonsense.

So we have:

  • AOT compiler: Compiled before running
  • JIT compiler: Compiled while executing
  • Interpreter: running

JIT compiler

Within the JIT compiler family, there are still many differences as to when exactly they are compiled, how often and with what granularity.

For example, the JIT compiler in Microsoft's CLR only compiles code once (when loaded) and compiles an entire assembly at a time. Other compilers may collect information while the program is running and recompile the code several times as new information becomes available to help them optimize it. Some JIT compilers can even write code de-optimize . Now you might be wondering why would you ever want to do that? Deoptimization allows you to do very aggressive optimizations that may be unsafe. If it turns out that you to Are aggressive, you can just step back, while with a JIT compiler that can't do the de-optimization, you couldn't do the aggressive optimizations in the first place.

JIT compilers can either compile a static unit of code at once (a module, a class, a function, a method, ...; these are, for example, usually called JIT Method at a time ) or they can use the momentum follow Find execution of code dynamically traces (typically loops) that they then compile (these are saved as Tracing GEG).

Combining interpreters and compilers

Interpreter and compiler can be combined into a single speech execution engine. There are two typical scenarios in which this is done.

An AOT compiler made up of the combination of X to Y with an interpreter for Y . In this case it is X usually a high level language that is optimized for human readability while Y A high level language is a compact language (often some type of bytecode) that is optimized for machine interpretability. For example, the CPython-Python execution module has an AOT compiler that compiles Python source code into CPython bytecode and an interpreter that interprets CPython bytecode. The YARV Ruby execution engine also has an AOT compiler that compiles the Ruby source code into the YARV bytecode, and an interpreter that interprets the YARV bytecode. Why do you want to do this Ruby and Python are both very high-level and somewhat complex languages, so we first compile them into a language that is easier to parse and interpret, and then interpret, that the language.

The other way to combine an interpreter and a compiler is to have an execution engine in the mixed mode . Here we have "mix" two "modes" of implementation the same Language together, ie an interpreter for X and a JIT compiler from X to Y . (The difference here is that in the above case we had several "phases" in which the compiler compiled the program and then fed the result into the interpreter. This is where the two work side by side in the same language. ) Code compiled by a compiler tends to execute faster than code executed by an interpreter. However, the actual compilation of the code takes some time (especially if you want to heavily optimize the code to be executed)very fast, it takes much Time). In order to bridge the time in which the JIT compiler is busy compiling the code, the interpreter can already start executing the code. Once the JIT has been compiled, we can switch execution to the compiled code. This means that we both get the best possible performance from the compiled code, but we don't have to wait for the compilation to complete and our application will run immediately (although not as quickly as possible).

This is actually just the simplest possible application of a mixed-mode execution engine. For example, it is more interesting not to start compiling right away, but to let the interpreter run a little and collect statistics, profile information, type information, information about the probability with which certain conditional branches are executed, which methods are called most frequently, etc. . and pass this dynamic information on to the compiler so that it can generate optimized code. This is also a way to implement the de-optimization mentioned above: if it turns out that you were too aggressive in optimizing, you can throw away some of the code and get back to interpretation. The HotSpot JVM does that, for example. It contains both an interpreter for JVM bytecode and a compiler for JVM bytecode. (Actually,two Compiler!)

It is also possible, and indeed common, to combine these two approaches: Two phases, the first being an AOT compiler, the X to Y compiled, and the second phase a mixed-mode engine, the Y interpreted and Y to Z compiled. For example, the Rubinius Ruby execution engine works like this: It has an AOT compiler that compiles Ruby source code into Rubinius bytecode, and a mixed-mode engine that interprets Rubinius bytecode first and, after collecting some information, the most commonly cited methods in native compiled machine language.

It should be noted that the role that the interpreter plays in the case of a mixed mode execution machine, namely providing a quick start and also potentially collecting information and providing fallback capabilities, alternatively also from a second JIT compiler can be taken over. This is how V8 works, for example. V8 never interprets, it always compiles. The first compiler is a very fast, very flat compiler that starts very quickly. However, the code generated is not very fast. This compiler also inserts profiling code into the code it generates. The other compiler is slower and uses more memory, but produces much faster code. It can use the profile information gathered by running the code compiled by the first compiler.