3

Ask HN: Recommendation for general purpose JIT compiler

 1 year ago
source link: https://news.ycombinator.com/item?id=31389024
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Ask HN: Recommendation for general purpose JIT compiler

Ask HN: Recommendation for general purpose JIT compiler
32 points by role 2 hours ago | hide | past | favorite | 22 comments
Are there any open-source "general purpose" JIT compiler's out there? "General purpose" in the sense that it is not tied to any programming language (unlike v8 and luajit). At a high level, I want to feed in IR and get out a function pointer to call.

My use case is I have a DSL with a custom parser and interpreter. The DSL is essentially a programming language and is proving too slow (in terms of latency). The bottlneck is in the interpreter. I want to replace the interpreter with a JIT without having to deal with assembly code generation myself.

Preferably in Rust and/or Rust bindings. Preferably lightweight (small object code footprint). Preferably cross-arch (x86, arm, arm64).

This is exactly the API presented by LLVM ORC and gccjit, you feed in their IR/bitcode and they return to you a C function pointer.

https://llvm.org/docs/ORCv2.html

https://gcc.gnu.org/onlinedocs/jit/

s.gif
+1 for LLVM ORC, it's really powerful but also very easy to use. There is a simple tutorial in C++ which is a great intro to it.
s.gif
And the Rust ecosystem has good tooling for working with llvm already since it uses llvm itself.
As folks have mentioned, LLVM's JIT produces great code and is (relatively) easy to use.

However, LLVM is extremely heavyweight. Which "latency" did you mean? Are you going to run these functions 1M times, so that the quality of the generated code is paramount (and you can afford really long compile time) or do you care more about "I hit enter and get the answer"? You can tune LLVM (disable almost all passes, use fast instruction selection) but it's really not focused on millisecond-ish compilation.

There are a lot of "simple JIT" libraries for the latter case (you just want to feed in a simple IR and get machine code out, do an okay job at register allocation, but nothing fancy). None of them has "won" and most only have C bindings (to my knowledge).

Haven't used the RPython toolchain, but it's worth looking into. You write an interpreter in a restricted subset of Python2 called RPython and make your interpreter report the start of a loop in the guest language, and the end of a loop. The interpreter then gets transformed effectively into a JIT compiler. The underlying principle is something called meta-tracing.

[edit: Changed "host" to "guest"; Python to Python2]

I have a question to the experts though: The principle of meta-tracing suggests you might be able to write your guest language in Python. Is that currently possible with RPython/PyPy?

s.gif
Basically the concept of rPython is you write your interpreter in rPython you get a jit for “free”. However your target language doesn’t have to be Python.

A list of rPython projects is here: https://rpython.readthedocs.io/en/latest/examples.html

I suppose you could do something similar like writing your interpreter in luaJIT. It’s not clear if rPython is doing anything special to enable jit in the target language but it is designed for this purpose.

GNU lightning is a simple and portable JIT library written in C, it might be a good fit. Its engine is fast and minimal, and does not perform much in the way of optimisations for you.

(For what it's worth I'm a very minor contributor.)

* https://en.wikipedia.org/wiki/GNU_lightning

* https://www.gnu.org/software/lightning/

If you want a more sophisticated JIT engine, others have already mentioned libgccjit and LLVM (heavyweight compiler solutions), as well as Cranelift and Mir (more lightweight).

Of these, only Cranelift is written in Rust.

I would personally try cranelift for this. Its goals are around producing executable code quickly rather than optimally, which is pretty much the opposite of llvm. There are lots of things out there that use llvm for jit, but even its jit library layer is pretty heavy and you’ll probably spend more time generating code than you’d like.

That said, cranelift is still experimental.

s.gif
I haven't used it, but cranelift is also my first thought for this.

If you want a stable interface though, I might use wasm on top of it via wasmtime. I'm not sure how stable the API for wasmtime is, but at least the IR (wasm) is, and there's an ecosystem of tools around it.

https://github.com/bytecodealliance/wasmtime

If your DSL is statically typed then I recommend that you have a look at the Mono CLR; it's compatible with the ECMA-335 standard and the IR (CIL) is well documented, even with secondary literature.

If your DSL is dynamically typed I recommend LuaJIT; the bytecode is lean and documented (not as good as CIL though). LuaJIT also works well with statically typed languages, but Mono is faster in the latter case. Even if it was originally built for Lua any compiler can generate LuaJIT bytecode.

Both approaches are lean (Mono about 8 MB, LuaJIT about 1 MB, much leaner and less complex than e.g. LLVM), general purpose, available on many platforms (especially the ones you're mentioning) and work well (see e.g. https://github.com/rochus-keller/Oberon/ and https://github.com/rochus-keller/Som/).

LLVM (in ORC mode) is very powerful. I wrote a JIT compiler for a DSL with it. It takes a fair bit of poring over the IR manual to figure out basic things. The optimization is as good as for C, including automatic unrolling of loops to generate SIMD instructions.

A very useful feature is that you can write C or C++ and run it through LLVM to see the IR it generates, and adapt it to your needs. You can even do it in Godbolt.

If your generated code is crunching over large amounts of data, an alternative to a JIT is to make the interpreter implicitly parallel. So each interpreter dispatch operation does N (say, 16) parallel operations, effectively cutting interpreter overhead by N. It works if there isn't data-dependent branching, which is often true for numerical operations.

s.gif
It is fairly easy to use and can produce machine code for any architecture that LLVM supports (and this includes x86, x86_64, ARM, MIPS, PowerPC and SPARC).
LibJIT is a library that provides generic Just-In-Time compiler functionality independent of any particular bytecode, language, or runtime:

https://www.gnu.org/software/libjit/

I've used that in the past to speed up a toy interpreter, but of course it is in C, rather than Rust.

There is at least one binding for it in rust:

https://github.com/MonliH/jit-sys

Finally here's a good introduction with several approaches for JIT:

https://eli.thegreenplace.net/tag/code-generation

There is Graal's Truffle [0]

You'll have to rewrite your parser/interpreter in Truffle, but you get everything else "for free".

Not in Rust. I wouldn't call it at all lightweight. It is cross-arch in the sense that the Graal JVM is cross-arch, which may or may not be sufficient for your purposes.

[0] https://www.graalvm.org/22.0/graalvm-as-a-platform/language-...

You could write JVM byte code based on some language that isn’t Java but then you get the garbage collector and the rest of the runtime which you may or may not like.
I would suggest generating webassembly instead and use wasmtime.
s.gif
Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search:

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK