In the examples up to now the top-of-stack has been kept in a
register, which already provides a good benefit compared to keeping
all stack items in memory (which is used by the gforth
engine). Here’s the payload (i.e., without NEXT) of * for
gforth-fast and gforth:
gforth-fast gforth SP=%r10, TOS=%r13 SP=%r14 mov 0x8(%r14),%rax imul 0x8(%r10),%r13 imul (%r14),%rax add $0x8,%r10 add $0x8,%r14 mov %rax,(%r14)
However, by using multiple stack representations, this can be improved
even more. In particular, gforth-fast uses one stack
representation (called 0) with no stack item in memory, one
(1) with one stack item in memory, and so on.
Gforth has variants of the most common primitives for the most useful transitions between stack representations. It uses a shortest-path algorithm to select a combination of primitive variants that fits together (i.e., where each primitive variant starts with the representation in which the previous primitive variant finished) and that results in the shortest native-code size (by default).
The transitions are shown as s->t), meaning that the
primitive variant starts with representation s and finishes in
representation t. One can also consider these as transitions in a
state machine, so we also talk about a stack representations as a
stack state.
For the squared example that results in:
<squared> dup 1->2 $7F04266893D7: add $0x8,%rbx $7F04266893DB: mov %r13,%r15 <squared+$8> * 2->1 $7F04266893DE: add $0x8,%rbx $7F04266893E2: imul %r15,%r13 <squared+$10> ;s 1->1 $7F04266893E6: mov (%r14),%rbx $7F04266893E9: add $0x8,%r14 $7F04266893ED: mov (%rbx),%rax $7F04266893F0: jmp *%rax
Here %13 contains the first register above the memory part of
the stack, %15 the second one. Note that it depends on the
representation which of the two is the top-of-stack: In 0 all
stack items are in memory, in 1 the TOS is in %r13, and
in 2 the TOS is in %r15 and the stack item below is in
%r13.
Multi-representation stack-caching reduces memory accesses as well as stack pointer updates.
gforth-fast falls back to the canonical representation
1 on control flow (including calls and exits), when
dealing with non-relocatable primitives, or when a static
superinstruction is selected.
Multi-state stack caching is controlled with
--ss-states=n, where n gives the number of states:
For n=1, only the canonical state 1 is used, for n=2
also state 0, and the next states enabled are 2 (for
n>=3) and 3 (for n>=4). On a number of platforms (in
particular, AMD64, ARM A64, RISC-V), gforth-fast supports 4
stack representations (0–3), on some others only 2
(0, 1); the canonical representation is always 1.
The code above was generated by using gforth-fast
--opt-ip-updates=0.
You can read more about static superinstructions in M. Anton Ertl, David Gregg, Stack Caching in Forth, EuroForth 2005 proceedings.