Contact
Search
Essays

Entries in NanoJIT (2)

Friday
Oct302009

Adding More Cowbell to Tamarin

Many dynamic language virtual machines (VM) written in C++ work by compiling code that passes parameters into VM C++ methods which do most of the heavy lifting. For example, adding two values becomes two x86 pushes onto the stack with a x86 call into a C++ method add. When I last looked at Apple's SquirrelFish, that's all it did. The generated x86 code in Tamarin more or less does the same thing with a few optimizations. And having such a simple compiler works surprisingly well, but it only takes you so far. Sooner or later, you'll want to generate better, faster machine code.

An effective way of generating faster code is method inlining. Instead of generating calls, the JIT should inline the methods that do the real work. The problem for Tamarin's JIT, is that it doesn't know how to inline C++ methods. NanoJIT only compiles LIR, not C++. Enter the C++ to LIR translator.

We do this by statically compiling Tamarin with LLVM. LLVM is a pretty awesome GCC replacement that is nicely designed, object oriented, and generally a pleasure to work with. The output of LLVM is like an object file, but instead contains LLVM bitcode. This new tool then translates LLVM bitcode into LIR. At runtime, Tamarin compiles the LIR, which really represents a VM C++ method. Boom, inlinable C++ methods.

We don't have to apply the translator to only method inlining. In essence, Tamarin compiles VM C++ methods as if they were ActionScript methods. Application ActionScript, such as a youtube video, prior to execution, is translated into LIR, which is then compiled into machine code. We also translate most of Tamarin, written in C++, into LIR, which is then compilable into machine code by NanoJIT. NanoJIT has the power to compile, inline, and optimize VM C++ methods for the performance win.

You may be thinking, hmm this sounds an awfully like a self-interpreting VM. A self-interpreting VM is a VM written in the language it executes. For example, writing a JavaScript VM in JavaScript would make it self-interpreting. Well, you're right. This approach hacks in self-interpreting/JITting into a VM written in C++.

Consider the three images above just to clarify the differences. The "standard" VM approach, and what Tamarin did before, is to JIT code that calls into C++ methods. The host C++ compiler (Visual Studio/GCC) does a lot of work, and the JIT has no idea what was going on. In a self-interpreting VM, the JIT compiles everything, application and VM code, and knows everything about itself. This approach mimics a self-interpreting VM by translating C++ methods into LIR, and JITing both application and VM code.

Over the next few weeks, I'll be detailing the implementation/design decisions in Tamarin.

Thanks to Michael Bebenita for proofreading and creating the images. Checkout this great SNL skit if you are wondering what More Cowbell is.

Sunday
Oct042009

LIR after the NanoJIT merge

When TraceMonkey was born, the team forked the NanoJIT backend from Tamarin. However, over the summer, the TraceMonkey and Tamarin teams wanted to merge their changes back into a shared repository. The intermediate representation (LIR) changed a bit. What's it look like now?

Here is what a basic LIR instruction looks like:

class LIns
{
        union {
            Reservation lastWord;
            // force sizeof(LIns)==8 and 8-byte alignment on 64-bit machines.
            // this is necessary because sizeof(Reservation)==4 and we want all
            // instances of LIns to be pointer-aligned.
            void* dummy;
        };
}

 
What about Reservation?

// The opcode is not logically part of the Reservation, but we include it
// in this struct to ensure that opcode plus the Reservation fits in a
// single word. 
struct Reservation
{
        uint32_t arIndex:16;    // index into stack frame.  displ is -4*arIndex
        Register reg:7;         // register UnknownReg implies not in register
        uint32_t used:1;        // when set, the reservation is active
        LOpcode  opcode:8;
}

A LIR instruction is a padding around a 32 bit Reservation which contains the opcode. But where are the operands to an instruction?

This is the biggest difference after the merge, at least from Tamarin's perspective. Prior to the nanoJIT merge, LIR instructions were inserted into a contiguous chunk of memory. Each LIR instruction was one 32 bit word. The top 8 bits were reserved for the opcode while the lower 24 bits were used as operands. Each operand was represented as an 8 bit offset from the point in memory. The actual LIR instruction structure had no notion of pointers.

Now LIR instructions directly point to their operands. But where are the operands?. There are multiple LIR instruction types depending on the number of operands an instruction requires. For example, return instructions only need to point to the value they are returning. NanoJIT has a LInspOp1 class for instructions that have only one operand:

// 1-operand form.  Used for LIR_ret, unary arithmetic/logic ops,
class LInsOp1
{
        friend class LIns;
        LIns*       oprnd_1;
        LIns        ins;
}

 

"Ins" here points to "this" instruction. NanoJIT also has LInsOp2 for instructions with two operands:

// 2-operand form.  Used for loads, guards, branches, comparisons, binary
class LInsOp2
{
        LIns*       oprnd_2;
        LIns*       oprnd_1;
        LIns        ins;
};

 

And one last one for instructions that have three operands. This means LIR instructions are variable length and can be up to 4 words in length. Now what about constant values such as the number 6? NanoJIT has a few other specialized LIR instructions such as LInsI:

// Used for LIR_int and LIR_ialloc.
class LInsI
{
        int32_t     imm32;
        LIns        ins;
};

 

If you want to get into the gritty details, checkout the NanoJIT merge MDC article. If you want to see how it is all implemented, checkout LIR.h on the mercurial repository on line 210 for a nice comment.