Which Interpreters Are Faster, AST or Bytecode?
stefan-marr.de
stefan-marr.de
If AST interpreters without a JIT perform better than reasonably-written bytecode interpreters via some technique or for some workload (other than just short-running scripts, which is fair), I'd love to learn about it!
I saw that somewhere before, let me see if I can find it.
EDIT: https://blog.cloudflare.com/building-fast-interpreters-in-ru...
There are a bunch of reasons we did eventually give up on the AST interpreter, but one of the biggest was that by the end we were seeing more than of 15% of runtime being just the call+return overhead. But there are a bunch of other things that make bytecode better: the AST interpreter is inherently recursive so it wasn't uncommon to have code that would fail in JSC due to stack overflows because an expression like `function f() {a(b(c(df()))}` would fundamentally 5x the recursive calls per js call (this is just to give you an idea of how recursive things were, the example above strictly always overflows). The bytecode also made it much easier to perform some inter-expression optimision under the latency requirements of a js interpreter, as well as reducing the GC costs with JSC (JSC uses a conservative collector that does stack scanning).
Combined with a meta-compilation system I believe it would be a fairly efficient way to implement a tree-walking interpreter.
[0] https://blog.reverberate.org/2021/04/21/musttail-efficient-i...
We report on results for both, with and without just-in-time compilation. The specific focus for this work was pure interpreter performance in the context of metacompilation systems, but before compilation had a chance to kick in.
You are free to disagree with the specific design choices of the AST and bytecode interpreters for SOM, but we put quite some effort in to have an as fair as possible comparison.
We report on results for both, with and without just-in-time compilation. The specific focus for this work was pure interpreter performance in the context of metacompilation systems, but before compilation had a chance to kick in.
For both RPython and Truffle/Graal, it's possible to disable the JIT compilers and measure pure interpreter speed.
So the "baseline" is Java - is that Java compiled or interpreted? And if the latter, is the non-JIT-ted Graal interpreter compiled (as Java) and interpreting the script, or is it interpreted itself?
The figure for the JIT-compiled numbers uses a standard HotSpot JVM, with JIT compilation. The figures for the interpreter numbers uses a standard HotSpot JVM with the -Xint flag, so, only using the Java bytecode interpreter.
The TruffleSOM interpreter is AOT-compiled, so, it's a native binary, which is then interpreting the SOM code.
It uses some custom language (SOM), built the AST/bytecode interpreters for that on top of other systems (GraalVM and RPython) which prohibits some optimizations and may use some intermediate format (Graal IR).
And then compares results using a limited set of benchmarks (Are We Fast Yet).
Interesting, but all in all it's not saying much (imho).
As so often: devil is in the details.
SOM is the language being implemented, it's not used to build the interpreters.
But please feel free to replicate the study for your preferred language. I am happy to discuss more about why we made certain choices.
https://en.wikipedia.org/wiki/Partial_evaluation#Futamura_pr...
(1) https://www.graalvm.org/latest/graalvm-as-a-platform/languag...
By the way there's an excellent series of articles about implementing an interpreter with Truffle by Adam Ruka: https://www.endoflineblog.com/graal-truffle-tutorial-part-12...
Definitely. The title of the linked paper is a lot more reasonable:
> AST vs. Bytecode: Interpreters in the Age of Meta-Compilation
it's a shame the author decided to go with a clickbait version for his blog entry.
You can get pretty decent performance for substantially less effort by reusing more features of the host language, enums or tail calling closures for ops, inlining values without bit twiddling etc.
I still feel there's a point in having two separate representations though, one for reasoning about the code at a higher level and one for execution; it also gives you a compilation phase as a consequence, which allows nailing decisions statically and avoiding run time performance hits.
Lisp makes an interesting implementation language, since it allows easily generating and compiling code straight to the host language, which adds another layer of interesting possibilities for optimization.