interpreters and virtual machines interpreters
play

Interpreters and virtual machines Interpreters Michel Schinz - PDF document

Interpreters and virtual machines Interpreters Michel Schinz 20070323 Interpreters Why interpreters? An interpreter is a program that executes another program, Interpreters enable the execution of a program without represented as some


  1. Interpreters and virtual machines Interpreters Michel Schinz 2007–03–23 Interpreters Why interpreters? An interpreter is a program that executes another program, Interpreters enable the execution of a program without represented as some kind of data-structure. requiring its compilation to native code. Common program representations include: They simplify the implementation of programming • raw text (source code), languages and – on modern hardware – are efficient • trees (AST of the program), enough for most tasks. • linear sequences of instructions. 3 4 Text-based interpreters Tree-based interpreters Tree-based interpreters walk over the abstract syntax tree Text-based interpreters directly interpret the textual source of the program to interpret it. of the program. Their advantage compared to string-based interpreters is They are very seldom used, except for trivial languages that parsing – and name/type analysis, if applicable – is where every expression is evaluated at most once – i.e. done only once. languages without loops or functions. Plausible example: a graphing program, which has to Plausible example: a calculator program, which evaluates repeatedly evaluate a function supplied by the user to plot arithmetic expressions while parsing them. it. 5 6

  2. Virtual machines Virtual machines behave in a similar fashion as real machines ( i.e. CPUs), but are implemented in software. They accept as input a program composed of a sequence of Virtual machines instructions. Virtual machines often provide more than the interpretation of programs: they manage memory, threads, and sometimes I/O. 8 Virtual machines history Why virtual machines? Since the compiler has to generate code for some machine, why prefer a virtual over a real one? Perhaps surprisingly, virtual machines are a very old • for simplicity: a VM is usually more high-level than a concept, dating back to ~1950. real machine, which simplifies the task of the compiler, They have been – and still are – used in the implementation of many important languages, like SmallTalk, Lisp, Forth, • for portability: compiled VM code can be run on many Pascal, and more recently Java and C#. actual machines, • to ease debugging and execution monitoring. 9 10 Virtual machines drawback Kinds of virtual machines There are two kinds of virtual machines: 1. stack-based VMs, which use a stack to store The only drawback of virtual machines compared to real intermediate results, variables, etc. machines is that the former are slower than the latter. 2. register-based VMs, which use a limited set of registers This is due to the overhead associated with interpretation: for that purpose, like a real CPU. fetching and decoding instructions, executing them, etc. There is some controversy as to which kind is better, but Moreover, the high number of indirect jumps in interpreters most VMs today are stack-based. causes pipeline stalls in modern processors. For a compiler writer, it is usually easier to target a stack- based VM than a register-based VM, as the complex task of register allocation can be avoided. 11 12

  3. Virtual machines input VM implementation Virtual machines are implemented in much the same way Virtual machines take as input a program expressed as a as a real processor: sequence of instructions. • the next instruction to execute is fetched from memory Each instruction is identified by its opcode ( op eration and decoded, code ), a simple number. Often, opcodes occupy one byte, overhead hence the name byte code . • the operands are fetched, the result computed, and the state updated, Some instructions have additional arguments, which appear after the opcode in the instruction stream. • the process is repeated. 13 14 VM implementation Implementing a VM in C typedef enum { add, /* ... */ } instruction_t; Many VMs today are written in C or C++, because these languages are at the right abstraction level for the task, fast void interpret() { static instruction_t program[] = { add /* ... */ }; and relatively portable. instruction_t* pc = program; As we will see later, the Gnu C compiler ( gcc ) has an int* sp = ...; /* stack pointer */ for (;;) { extension that makes it possible to use labels as normal switch (*pc++) { values. This extension can be used to write very efficient case add: VMs, and for that reason, several of them are written for sp[1] += sp[0]; sp++; gcc . break; /* ... other instructions */ } } } 15 16 Optimising VMs The basic, switch -based implementation of a virtual machine just presented can be made faster using several techniques: Threaded code • threaded code, • top of stack caching, • super-instructions, • JIT compilation. 17

  4. Threaded code Threaded code vs. switch Program: add sub mul In a switch -based interpreter, each instruction requires switch -based Threaded two jumps: main loop main 1. one indirect jump to the branch handling the current instruction, 2. one direct jump from there to the main loop. add add It would be better to avoid the second one, by jumping directly to the code handling the next instruction. This is sub sub called threaded code . mul mul 19 20 Implementing threaded code Threaded code in C To implement threaded code, there are two main techniques: To implement threaded code, it must be possible to • with indirect threading , instructions index an array manipulate code pointers. How can this be achieved in C? containing pointers to the code handling them, • In ANSI C, the only way to do this is to use function • with direct threading , instructions are pointers to the pointers. code handling them. • gcc allows the manipulation of labels as values, which Direct threading is the most efficient of the two, and the is much more efficient! most often used in practice. For these reasons, we will not look at indirect threading. 21 22 Direct threading in ANSI C Direct threading in ANSI C typedef void (*instruction_t)(); static instruction_t* pc; static int* sp = ...; static void add() { sp[1] += sp[0]; Implementing direct threading in ANSI C is easy, but ++sp; unfortunately very inefficient! (*++pc)(); /* handle next instruction */ } The idea is to define one function per VM instruction. The program can then simply be represented as an array of /* ... other instructions */ function pointers. Some code is inserted at the end of every static instruction_t program[] = { add, /* ... */ }; function, to call the function handling the next VM instruction. void interpret() { sp = ...; pc = program; (*pc)(); /* handle first instruction */ } 23 24

  5. Direct threading in ANSI C Trampolines This implementation of direct threading in ANSI C has a major problem: it leads to stack overflow very quickly, unless the compiler implements an optimisation called tail It is possible to avoid stack overflows in a direct threaded call elimination ( TCE ). interpreter written in ANSI C, even if the compiler does not perform tail call elimination. Briefly, the idea of tail call elimination is to replace a function call that appears as the last statement of a function The idea is that functions implementing VM instructions by a simple jump to the called function. simply return to the main function, which takes care of calling the function handling the next VM instruction. In our interpreter, the function call appearing at the end of add – and all other functions implementing VM While this technique – known as a trampoline – avoids instructions – can be optimised that way. stack overflows, it leads to interpreters that are extremely slow. Its interest is mostly academic. Unfortunately, few C compilers implement tail call elimination in all cases. However, gcc 4.01 is able to avoid stack overflows for the interpreter just presented. 25 26 Direct threading in ANSI C Direct threading with gcc typedef void (*instruction_t)(); static int* sp = ...; static instruction_t* pc; static void add() { sp[1] += sp[0]; The Gnu C compiler ( gcc ) offers an extension that is very ++sp; useful to implement direct threading: labels can be treated ++pc; } as values, and a goto can jump to a computed label. /* ... other instructions */ With this extension, the program can be represented as an array of labels, and jumping to the next instruction is static instruction_t program[] = { add, /* ... */ }; achieved by a goto to the label currently referred to by the void interpret() { program counter. sp = ...; pc = program; for (;;) (*pc)(); trampoline } 27 28 Direct threading with gcc Threading benchmark To see how the different techniques perform, several versions of a small interpreter were written and measured label as value while interpreting 100’000’000 iterations of a simple loop. void interpret() { void* program[] = { &&l_add, /* ... */ }; All interpreters were compiled with gcc 4.0.1 with maximum optimisations, and run on a PowerPC G4. int* sp = ...; void** pc = program; The normalised times are presented below, and show that goto **pc; /* jump to first instruction */ only direct threading using gcc ’s labels-as-values performs l_add: better than a switch-based interpreter. computed sp[1] += sp[0]; goto ++sp; switch 1.00 goto **(++pc); /* jump to next instruction */ ANSI C, without TCE 1.80 /* ... other instructions */ } ANSI C, with TCE 1.45 gcc’s labels-as-values 0.61 0 0.5 1.0 1.5 2.0 29 30

Recommend


More recommend