3 ms·
You make a very astute point. I'd like to expand on this a bit. The key thing to note between traditional stack-ops and register-ops is that opcode space. St
by kannanvijayan 12y ago
You make a very astute point. I'd like to expand on this a bit.
The key thing to note between traditional stack-ops and register-ops is that opcode space. Stack machines are typically designed with a small number of primitive ops that operate on the top of the stack. For example, adding two local vars and storing them into a local var might look like:
In source code:
x = x + y;
In stack ops:
getlocal 0
getlocal 1
add
setlocal 0
In register ops:
add r0, r1, r0
The key thing to note is that the "add" instruction in the register machine encodes a lot more information. Given a register pool of 32, for example, it encodes 15 bits of extra information (3 5-bit ops) than the "add" instruction in the stack example. The corresponding "getlocal" and "setlocals" are collapsed into the add itself.
As you point out, there's NOTHING stopping a stack machine designer from doing the same thing. It's just traditionally not done, perhaps due to some inclination to keep things "clean and orthogonal".
One could imagine a stack opcode space that provided the following add instructions:
add_ppp - traditional "stack machine" add. Pop, pop, push.
add_lll <R> <L> <X> - extended "locals" add. Load rhs and lhs from locals R and L, store to local X.
add_ppl <X> - hybrid. pop rhs and lhs from stack, store result in local X.
add_ssl <R> <L> <X> - hybrid. treat the stack as a register file indexed from the top. Load rhs and lhs from stack locations R and L, store result into local variable X.
and so on and so on.
One could imagine for every binary operation, there would be a number of variants:
op_IIR
where I could be any of {p, s, l, a, c} for {pop stack, peek stack, load localvar, getarg, constant pool}, and R could be any of {p, l, a, v} for {push stack, store localvar, setarg, and void (discard)}.
I've run measurements of how a hypothetical opcode space would influence the size and number-of-ops for expressions, and basically the result is that it's strictly better than both. It produces smaller code than typical stack machines, and fewer number of instructions than typical register machines.
Basically by inflating the opcode space in a stack machine, and using it to encode operand information, you can achieve the best of both worlds. You get to use the stack as an implicit scratchspace when computing intermediates in expressions, and you get to use your constant pool and locals and arguments directly when computing expression leaves and when storing expression results.