This week I've been working on a JIT X86 code generator for Neko. After 4 days most of the opcodes are implemented and it seems to work very well. Also, this have been possible with only very few changes in the VM itself since the JIT engine is entirely written in NekoML, so it's actually a Neko Library (while most JIT engines are usually written in C and are part of the VM).
At first, I was thinking about a two-layers approach, one first generator that will translate the Neko bytecode into abstract CPU-independent opcodes an then a second generator that will generate the corresponding X86 opcodes. After two days I realized that my abstract opcodes where actually very near from X86 ones, for the good reason that I didn't know what exactly should be abstracted.
I was studying the X86 opcodes at the same time I was writing the JIT. Although I made a little ASM back to my Pascal days, I didn't know at all the opcodes binary representations (quite difficult actually since there is a lot of different forms for the same opcode). Since I don't know precisely the opcodes for others platforms where JIT would be ported (AMD64 ?), it was a bad idea from the beginning to abstract something I didn't know about.
So I ended-up merging my two-layers into a one-layer generator, with a small separate X86 library that is taking care of binary representation of opcodes. Basically, the JIT is doing the following things :
- read the module that need to be JIT. This is done using the module API of Neko standard library which allow to read and safely manipulate a module without initializing it.
- from decoded integers representing opcodes, rebuild the NekoML opcodes (this require some NekoML magic)
- with each opcode, generate corresponding X86 opcodes : you need to keep some VM state in registers (stack, accumulator, VM instance...). It means actually rewriting the VM bytecode interpreter loop in assembler.
- use the X86 library to build a string representing the opcodes
- use the
$jitbuiltin to execute the code : this builtin is the only unsafe builtin in all Neko. It means that you can only crash the VM by using this builtin. For , it might be nice to change it into a primitive, but right now it's more easy to debug this way.
Now that the code is generated, it's not finished. The Bytecode Functions are stored into the module globals table, so if you run the JIT-code now, all function calls with go back to the bytecode interpreter instead of running the JIT version of the function. It was then needed to introduce a new type of function (after "bytecode-function" and "C primitive") which would callback into JIT, and transform all module globals storing bytecode functions into the corresponding JIT-function.
Works like a charm.
However there still a few things left :
- implement missing Neko opcodes (mainly numeric operations) plus the
JmpTableopcode which is more difficult
- right now the VM cannot callback JIT-functions, and before they can actually start executing the JIT code, it is needed to correctly setup the registers for JIT-mode
- same for exceptions : right now when an exception is catched, the control will return to the bytecode version of the module instead of the JIT version
After that is done, it will be pretty nice since both JIT and VM interp can callback each other transparently while still keeping nice Neko features such as stack traces fully working.
You can watch the JIT sources in
nekoCVS/src/jit/Jit.nml, that's only a 26K NekoML file right now.