13.4 Stack caching

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 (03), 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.