Earlier quoted context omitted.
Do you know if an optimizing compiler will ever make this transformation?
Every single C compiler worth mentioning will turn a switch statement into a jump table. The difference between a jump table and computed goto is only that one is `jmp [ecx+eax 8]` and the other is `mov edx, [ecx+eax 8]; jmp edx`. The later is faster because of weird branch prediction reasons, and no bounds checks in the switch.
Write Your Own Virtual Machine
51–60 of 79 posts
Re: Write Your Own Virtual Machine
#52Earlier quoted context omitted.
Better him than me. I wouldn't be so quick to dismiss relevant coding experience, it doesn't get old. Much of his reasoning about locality still holds, to an even greater degree today. I've implemented more or less the same interpreter using computed goto and various variations on dispatch loops; and this is the fastest solution I've managed to come up with, and the nicest code to work with. Still too many parameters…
Oh, I wasn't dismissing experience in general, nor was I questioning your experiences. I would love to see your numbers on the different dispatch methods you tried. I was only questioning that one blog post you linked that lazily said (paraphrasing) "computed-goto dispatch will be undone by the compiler, so don't bother". Others have posted numbers in this thread ( https://news.ycombinator.com/item?id=18679477 ) show…
Sometimes the reason no one finds a better solution for a long time is that no one believes it's possible. Let's say it's 50/50, believing it's possible is still the superior alternative.
I have no idea what Python uses for regular dispatch; the linked thread just says that their computed goto solution is faster, which comes as no surprise given that was the reason they switched.
I posted the link since I learnt a lot from it, and since it echoes my own experience.
Re: Write Your Own Virtual Machine
#53I agree it's a great exercise. Years ago I added a very simple stack-based VM to my raytracer to allow procedural textures & normal maps. The scene script would e.g. contain a material description like this: material { diffuse rgb = [0, 0, 1] * (1-(0.5+0.5*noise(x*0.000002, y*0.000002, z*0.000002, 0.66, 2))) + [1, 1, 1] * (0.5+0.5*noise(x*0.000002, y*0.000002, z*0.000002, 0.66, 2)); } i.e. an expression with 3-vector…
If you're comfortable sharing, that is.
Re: Write Your Own Virtual Machine
#54I agree it's a great exercise. Years ago I added a very simple stack-based VM to my raytracer to allow procedural textures & normal maps. The scene script would e.g. contain a material description like this: material { diffuse rgb = [0, 0, 1] * (1-(0.5+0.5*noise(x*0.000002, y*0.000002, z*0.000002, 0.66, 2))) + [1, 1, 1] * (0.5+0.5*noise(x*0.000002, y*0.000002, z*0.000002, 0.66, 2)); } i.e. an expression with 3-vector…
I don't suppose you have code or examples lying around? I'd love to see what you were trying to achieve, the results you had, and the code. If you're comfortable sharing, that is.
Re: Write Your Own Virtual Machine
#55Earlier quoted context omitted.
I don't suppose you have code or examples lying around? I'd love to see what you were trying to achieve, the results you had, and the code. If you're comfortable sharing, that is.
I still have the code (old and ugly, so not on GitHub; but it still builds and works) and examples. Let me know where I should send it.
Re: Write Your Own Virtual Machine
#56Off-topic. Does anyone know how I can convert AST into stream of bytecodes? Are there any good example language implementations to learn?
Re: Write Your Own Virtual Machine
#57One of my favorite techniques for implementing VMs is the "computed goto": https://eli.thegreenplace.net/2012/07/12/computed-goto-for-e... Consider this example for dispatching instructions from the article: while (running) { uint16_t op = mem_read(reg[R_PC]++) >> 12; switch (op) { case OP_ADD: {ADD, 6} break; case OP_AND: {AND, 7} break; case OP_NOT: {NOT, 7} break; case OP_BR: {BR, 7} break; ... } } That code has a…
Re: Write Your Own Virtual Machine
#58One of my favorite techniques for implementing VMs is the "computed goto": https://eli.thegreenplace.net/2012/07/12/computed-goto-for-e... Consider this example for dispatching instructions from the article: while (running) { uint16_t op = mem_read(reg[R_PC]++) >> 12; switch (op) { case OP_ADD: {ADD, 6} break; case OP_AND: {AND, 7} break; case OP_NOT: {NOT, 7} break; case OP_BR: {BR, 7} break; ... } } That code has a…
That suggests your C compiler isn't doing jump threading: replacing the jump to an unconditional jump instruction with a direct jump to that instruction's destination.
That's a very basic optimization I might expect even from a C compiler written in 1980.
Re: Write Your Own Virtual Machine
#59Earlier quoted context omitted.
I still have the code (old and ugly, so not on GitHub; but it still builds and works) and examples. Let me know where I should send it.
My email is in my profile.
Re: Write Your Own Virtual Machine
#60Off-topic. Does anyone know how I can convert AST into stream of bytecodes? Are there any good example language implementations to learn?
The code is very clean, and fairly suitable for study purposes. It is not commented, though.
http://www.kylheku.com/cgit/txr/tree/share/txr/stdlib/compil...
http://www.kylheku.com/cgit/txr/tree/share/txr/stdlib/asm.tl
http://www.kylheku.com/cgit/txr/tree/vm.c
The assembler accepts programs in a kind of Lisp syntax. It allows for symbolic labels and performs all the backpatching. The binary instruction format is written out with the help of the buf data type and some ffi-related functions for reading and writing binary types to and from a buffer.
Because the assembly code a Lisp data structure, the compiler can emit the instruction templates using backquote templates. Pieces of code can be catenated with append since they are just a list and so on.
The compiler comp-if method compiles (if ...) forms. The most general case with all three arguments (if expr then else) uses this code template:
^(,*te-frag.code
(if ,te-frag.oreg ,lelse)
,*th-frag.code
,*(maybe-mov oreg th-frag.oreg)
(jmp ,lskip)
,lelse
,*el-frag.code
,*(maybe-mov oreg el-frag.oreg)
,lskip)
The first element ,[star]te-frag.code splices in the code part of the fragment obtained from compiling the test expression. That code has to be run first. Then we emit the (if ...) instruction which tests the te-frag.oreg: the output register where the te-frag.code has stored the value. If the test is successful, the instructions continue below, otherwise a branch takes place to the else part. The else part is identified by lelse which holds the label: the ,lelse backquote syntax inserts that label into the template. So after the (if ...) instruction we splice the th-frag.code: the "then" fragment. After that we jump past the else part to whatever follows via (jmp ,lskip): jump to the skip label. Before this, we potentially insert a mov instruction that may be needed. We are expected to produce the result value of the (if ...) expression in an output register held in oreg. But the th-frag.code puts the result into th-frag.oreg: its own output register. Now those two may be the same. If they are not the same register, then a move is needed: the maybe-mov function produces the code when the registers are different, or else an empty list.In action:
This is the TXR Lisp interactive listener of TXR 203.
Quit with :quit or Ctrl-D on empty line. Ctrl-X ? for cheatsheet.
1> (compile-toplevel '(if x y z))
** warning: (expr-1:1) unbound variable x
** warning: (expr-1:1) unbound variable y
** warning: (expr-1:1) unbound variable z
#
2> (disassemble *1)
data:
syms:
0: x
1: y
2: z
code:
0: A0020000 getlx t002 0
1: 48000005 if t002 5
2: 00000002
3: A0020001 getlx t002 1
4: 44000006 jmp 6
5: A0020002 getlx t002 2
6: 10000002 end t002
instruction count:
6
#
Here, getlx t002 0 means look up the lexical variable named by the symbol at position [0] in the syms table, in other words x. Put the value into register t002. Then if t002 5 means, if t002 is true (non-nil), then continue, else branch to the instruction at offset 5.end t002 means that the VM is done executing and its result value is in t002.
We can intercept the call to the assembler asm method:
3> (trace (meth sys:assembler sys:asm))
nil
4> (compile-toplevel '(if x y z))
** warning: (expr-4:1) unbound variable x
** warning: (expr-4:1) unbound variable y
** warning: (expr-4:1) unbound variable z
((meth sys:assembler
sys:asm) (#S(sys:assembler buf #b''
bstr # sys:max-treg 0 sys:labdef #H(())
sys:labref #H(()))
((sys:getlx (t 2) 0) (if (t 2) #:l0019) (sys:getlx (t 2) 1) (sys:jmp #:l0020)
#:l0019 (sys:getlx (t 2) 2) #:l0020 (end (t 2))))
nil)
#Here the code looks like:
((sys:getlx (t 2) 0)
(if (t 2) #:l0019)
(sys:getlx (t 2) 1)
(sys:jmp #:l0020)
#:l0019
(sys:getlx (t 2) 2)
#:l0020
(end (t 2)))
There is a (t 2) syntax for the t002 register, labels are uninterned symbols like #:l0019.The assembler just works with that. It has an object for each opcode which knows how to encode it into the instruction buffer, plus logic for backpatching labels. When a forward jump is assembled, the label is added to a list of places needing backpatches along with a function to do it; later when the label is defined, the matching places are patched with the now known offset. The assembler contains very little stuff: buf is the buffer holding the output code (initially empty so it shows up as #b''). There is a bstr which is a stream over that buffer so we can do file-like I/O on the buffer. labref and labdef are hash tables for the label backpatching, and max-treg keeps track of the maximum T register number seen. The VM description emitted by the assembler will record this. When the VM is run, it just allocate only as many T registers on the stack as the code requires.