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.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!
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...
https://eli.thegreenplace.net/2012/07/12/computed-goto-for-e... - article about them being used in CPython
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.
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.
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...