13.2 Dynamic Superinstructions

The native code of the primitives in our squared example can be copied, with the copies being concatenated. Because the NEXTs of the primitives dup and * then just jump to the addresses right behind, the last two NEXT instructions of each of these primitives can be left away, resulting in:

$7F2BFA49D350: add    $0x8,%rbx      \ dup NEXT IP update
$7F2BFA49D354: mov    %r13,(%r10)    \ dup
$7F2BFA49D357: sub    $0x8,%r10      \ dup
$7F2BFA49D35B: add    $0x8,%rbx      \ * NEXT IP update
$7F2BFA49D35F: imul   0x8(%r10),%r13 \ *
$7F2BFA49D364: add    $0x8,%r10      \ *
$7F2BFA49D368: mov    (%r14),%rbx    \ ;s load IP from return stack
$7F2BFA49D36B: add    $0x8,%r14      \ ;s update return stack pointer
$7F2BFA49D36F: mov    (%rbx),%rax    \ ;s NEXT
$7F2BFA49D372: jmp    *%rax          \ ;s NEXT

The NEXT instructions of ;s cannot be left away, because ;s does not fall through to the next primitive, but performs control flow and needs to jump to the native code corresponding to the target threaded code; and it gets the code address of that native code through a regular direct-threaded code dispatch.

The threaded code for squared now points to the start of the respective primitives in the copied code, not to the original instances of this code; e.g., the first cell in squared now points to $7F2BFA49D350, the second to $7F2BFA49D35B, and the third to $7F2BFA49D368.

See-code displays the threaded code interleaved with the native code:

see-code squared 
<squared>     dup    1->1 
   $7F2BFA49D350: add    $0x8,%rbx
   $7F2BFA49D354: mov    %r13,(%r10)
   $7F2BFA49D357: sub    $0x8,%r10
<squared+$8>  *    1->1 
   $7F2BFA49D35B: add    $0x8,%rbx
   $7F2BFA49D35F: imul   0x8(%r10),%r13
   $7F2BFA49D364: add    $0x8,%r10
<squared+$10> ;s    1->1 
   $7F2BFA49D368: mov    (%r14),%rbx
   $7F2BFA49D36B: add    $0x8,%r14
   $7F2BFA49D36F: mov    (%rbx),%rax
   $7F2BFA49D372: jmp    *%rax

But note that the threaded code and native code are not interleaved in memory; <squared> is $7F2BFAAA7FE0, more than 6MB away from the native code.

An example with more control flow is

: mymin 2dup < if drop else nip then ;  ok
see-code mymin 
<mymin>       2dup    1->1 
   $7F2BFA49D374: add    $0x8,%rbx
   $7F2BFA49D378: mov    0x8(%r10),%rax
   $7F2BFA49D37C: sub    $0x10,%r10
   $7F2BFA49D380: mov    %r13,0x10(%r10)
   $7F2BFA49D384: mov    %rax,0x8(%r10)
<mymin+$8>    <    1->1 
   $7F2BFA49D388: add    $0x8,%rbx
   $7F2BFA49D38C: add    $0x8,%r10
   $7F2BFA49D390: cmp    %r13,(%r10)
   $7F2BFA49D393: setl   %r13b
   $7F2BFA49D397: movzbl %r13b,%r13d
   $7F2BFA49D39B: neg    %r13
<mymin+$10>   ?branch    1->1 
<mymin+$18>   <mymin+$38> 
   $7F2BFA49D39E: add    $0x10,%rbx
   $7F2BFA49D3A2: add    $0x8,%r10
   $7F2BFA49D3A6: test   %r13,%r13
   $7F2BFA49D3A9: mov    -0x8(%rbx),%rsi
   $7F2BFA49D3AD: mov    (%r10),%r13
   $7F2BFA49D3B0: jne    0x7f2bfa49d3ba
   $7F2BFA49D3B2: mov    (%rsi),%rax
   $7F2BFA49D3B5: mov    %rsi,%rbx
   $7F2BFA49D3B8: jmp    *%rax
<mymin+$20>   drop    1->1 
   $7F2BFA49D3BA: add    $0x8,%rbx
   $7F2BFA49D3BE: mov    0x8(%r10),%r13
   $7F2BFA49D3C2: add    $0x8,%r10
<mymin+$28>   branch    1->1 
<mymin+$30>   <mymin+$40> 
   $7F2BFA49D3C6: add    $0x10,%rbx
   $7F2BFA49D3CA: mov    -0x8(%rbx),%rbx
   $7F2BFA49D3CE: mov    (%rbx),%rax
   $7F2BFA49D3D1: jmp    *%rax
<mymin+$38>   nip    1->1 
   $7F2BFA49D3D3: add    $0x8,%rbx
   $7F2BFA49D3D7: add    $0x8,%r10
<mymin+$40>   ;s    1->1 
   $7F2BFA49D3DB: mov    (%r14),%rbx
   $7F2BFA49D3DE: add    $0x8,%r14
   $7F2BFA49D3E2: mov    (%rbx),%rax
   $7F2BFA49D3E5: jmp    *%rax

If compiles a ?branch with the target address as immediate argument, and else compiles a branch with the target address as immediate argument, while both else provides the target address for the ?branch and then provides the target addres for the branch. Both branch primitives are control flow words, load the code address from the target address and jump there if the branch is taken. If the ?branch is not taken, its native code branches around this control-flow code and falls through to the following primitive.

The name “superinstruction” means that a sequence of primitives (or VM instructions) is combined into one VM instruction, meaning it performs one threaded-code NEXT. “Dynamic superinstruction” means that the superinstructions are not formed at Gforth build time, but at Gforth run time. In the mymin example, we have superinstructions for the sequence 2dup < ?branch, for drop branch and for nip ;s. But note that the ?branch does not always perform a NEXT, so one might also see 2dup < ?branch drop branch as a superinstruction. Moreover, in some cases only the ;s part of the nip ;s superinstruction is performed.

The code copying involved in dynamic superinstructions cannot be performed with all primitives, only with those that are relocatable. If a primitive is not relocatable, the primitive before it has to end with the NEXT instructions, and the threaded-code for that primitive points to the original location of the primitive, like in ordinary threaded code. I.e., this approach can always fall back to threaded code if something is amiss. Fortunately, the most frequently used primitives are usually relocatable.

The code above was generated by using gforth-fast --ss-states=1 --ss-number=0 --opt-ip-updates=0; this disables the optimizations discussed in the next sections.

You can disable dynamic superinstructions with --no-dynamic, but that also disables several other optimizations.

The general approach of copying fragments of native code that have been generated by a C compiler is called code-copying compilaton.

You can read more about the code-copying used in Gforth in: M. Anton Ertl, Bernd Paysan, Code-Copying Compilation in Production — An Experience Report, EuroForth 2025 proceedings.