Writing a register based VM in less than 125 lines of C code
andreinc.net
andreinc.net
Did the seemingly-off-by-one-error catch anyone else? A 16-bit integer has 65536 values, so you'll be going one past the end of the virtual RAM if you attempt to access address FFFFh.
With this simple trick, we can avoid writing a switch statement with 16 (+1) cases
Personally, I prefer having a switch since it's more obvious which opcode goes where. Probably makes the code a little shorter too (and is that another off-by-one, this time in the other direction?)
Also, I understand the need to keep a "learning architecture" simple, but without byte-wide memory access, you'll quickly realise how annoying it is to work with actual bytes. For example, any string handling, unless you decide to use UTF-16 instead. I think a sweet spot is 16-bit address and 8-bit data, with register pairs for 16-bit operations, much like the popular Z80 and the 8080/8085 that preceded it.
The DEC Alpha is a real architecture that made a similar mistake and later had to be extended to fix it: https://en.wikipedia.org/wiki/DEC_Alpha#Byte-Word_Extensions...
In practice, having a switch is not worse or can even be better for performance because any non-naive optimising compiler will emit a nicely ordered indexed jump table, whereas following function pointers involves more indirection through the addresses of the functions, and has the additional overhead of passing args and preparing/restoring the stack.
But from a performance perspective, you are right!
https://eli.thegreenplace.net/2012/07/12/computed-goto-for-e... - article about them being used in CPython
Anton Ertl of Gforth fame published a microbenchmark performance comparison of toy interpreters, written in C, run on various CPUs. [1] As you say, the approach using function pointers (call threading) scores poorly.
Interestingly, direct threading and indirect threading are extremely close, with the winner seeming to depend on the specific CPU. [1] Branch-prediction differences seem to be the main reason. There was a 2001 paper on this. [2]
[0] https://www.complang.tuwien.ac.at/forth/threaded-code.html
[1] https://www.complang.tuwien.ac.at/forth/threading/
[2] https://github.com/ForthPapersMirror/Papers/blob/master/Conv...
Even if you don't want to call it UTF-16, you can just ignore the upper byte of your 16-bit cells and pretend they only store bytes when that's more convenient for you: for practical purposes you won't end up with any less memory available than if your cells were only 8 bits wide, but you have the option of using more when you need it.
uint16_t mem[UINT16_MAX + 1] = {0};
is it guaranteed to work on platforms where unsigned int is 16-bit wide? What about platforms where void* is 16-bit wide (hint: &mem[65536] must be a valid pointer value that must compare unequal from &mem[0])?explain
Moreover, if the expression P points to the last element of an array object,
the expression (P)+1 points one past the last element of the array object,
and if the expression Q points one past the last element of an array object,
the expression (Q)-1 points to the last element of the array object. If both
the pointer operand and the result point to elements of the same array object,
or one past the last element of the array object, the evaluation shall not
produce an overflow; otherwise, the behavior is undefined. If the result points
one past the last element of the array object, it shall not be used as the
operand of a unary * operator that is evaluated.This is visible in form of big split in Alpha hw support (drivers etc) on VMS and OSF/1 between "non-BWX" and "BWX" hardware.
I've never understood why modern software projects use names like `mw` or `mr`? What's wrong with `memory_read` / `memory_write`? Or `opcode_execute_fn opcode_executers[NOPS]` is magnitudes clearer than `op_ex_f op_ex[NOPS]`, and the only advantage of the latter is less time to write.
You will spend at least twice as much time reading your code as writing it, just take the extra time. Nobody needs another `vsnprintf_s`.
ETA: I have to repeat this is an insanely cool project idea, and very good execution
It's easier to pattern match strings that are short.
Having one `mr`/`mw` is confusing, but taking up whole lines with long strings can obscure the logic of the code.
mw(cpu.r[0], mr(mr(cpu.r[1])));
memory_write(processor.register[0], memory_read(memory_read(processor.register[1])));Initially mw was mem_write and mr was mem_read, and reg was registers, etc. But because I wanted to keep the functions as short as possible (to put them on the single line without making them totally unreadable), I've comeup with those names, which are confusing at first.
` memory_write(
processor.register[0],
memory_read(
memory_read(processor.register[1])
)
)
`To me, multiline statements are much more readable than single line nested statements regardless of abbreviations used.
Double-indirect through memory? I think that's something only VAX has.
* Would be nice to have a TOC for such a long article.
* I never implemented a register based VM but I think I remember one of the trickiest things is to allocate the registers when compiling from a higher level language [1], which I don't think is mentioned in the article? I think the article doesn't concern with this since the sample program just uses the bytecode directly.
* Random: been thinking of a DSL that would be able to express a VM logic without having to deal with all the micro details of C... Just found a few academic papers on a quick search investigating the idea, fwiw. [2] [3]
1: https://en.wikipedia.org/wiki/Register_allocation
2: https://link.springer.com/chapter/10.1007/978-3-540-25935-0_...
You are right about the TOC, my other long articles have them, but for this one I simply forgot to generate one. I will generate it tomorrow.
There's no compiler, and yes you are right managing a limited set of registers is hard when compiling from higher level languages. But there are a few compilers for LC3, so it's not impossible to write them even for such a limiting number of registers. But this is not my area of expertise, so someone more knowledgeable can comment on that.
[0] https://hal-lara.archives-ouvertes.fr/hal-02102286/file/RR20...
> The code is written in C11, and it will probably compile on most operating systems. The repo can be found here, and the exact source code is vm.c:
The link on "here" is broken, it's https://github.com/,nomemory/lc3-vm (with a comma before your github username) instead of (I think) https://github.com/nomemory/lc3-vm.
I’ll definitely be digging into this article!
I'd call it an interpreter/emulator for a simple instruction set and hardware interface.