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.