14.3 Native code of primitive variants

If you see-code a colon definition in gforth, for the primitives in the colon definition you see native code that is copied from the code that you see for the primitive, without or with the part of the NEXT at the end.

If you see-code colon definitions in gforth-fast, you often see code coming from other variants of the primitive, often without the IP update, or with a different IP update. This code is copied from other variants of the primitive in the engine. There is no simple way to see these variants, but starting the engine with --print-prims prints out information about all primitive variants when ending the session. If you are interested in a particular primitive, you can grep for it in the output, e.g.:

$ gforth-fast --print-prims -e bye |& grep '^[+] '
+                 1-1  0   67    0   77 0x5555555bc427 len= 4+  7+ 3 send=0
+                 2-1  0  802    0  189 0x5555555c24f9 len= 4+  3+ 3 send=0
+                 3-2  0  803    0   77 0x5555555c2505 len= 4+  3+ 3 send=0

The first column contains the name of the primitive, the second the stack-state transition. Then comes the IP offset (discussed below), next the primitive index (just a number), then (if non-zero) the index of the branch-to-IP variant discussed below, then the number of instances of this primitive variant in the generated native code, then the code address of the native code followed by its length, and finally an indication of whether the primitive ends a dynamic superinstruction.

The length is split into three parts: first the IP update, then the payload, and finally the load of the code address; the actual jump is not included (that’s a workaround bad code generation in some gcc versions).

In the above you see that there are three variants of + with different stack-state transitions (see Stack caching), that the 2-1 variant is generated more often than the other two combined, and that the 1-1 variant has longer native code than the others.

If you want to use the address and the length to disassemble the primitive variant, you usually need to invoke Gforth on Linux with setarch -R gforth-fast to disable ASLR (or equivalent commands on other OSs), on both the --print-prims run and the following disassembling session.

E.g., you can disassemble the payload of + 3-2 (index 803) with:

0x5555555c2505 4 + 3 discode 
   $5555555C2509: add    %r9,%r15

Here the 4 is the length of the code before the payload, the 3 is the length of the payload.

Concerning the IP offset, consider the following:

$ gforth-fast --print-prims -e bye |& grep '^lit '
lit               1-1  0   66    0    0 0x5555555bc413 len= 4+ 11+ 3 send=0
lit               0-1  0  659    0  351 0x5555555c1a4b len= 4+  4+ 3 send=0
lit               0-1  1  660    0    1 0x5555555c1a58 len= 4+  3+ 3 send=0
lit               0-1  2  661    0 1841 0x5555555c1a64 len= 4+  4+ 3 send=0
lit               0-1  3  662    0  340 0x5555555c1a71 len= 4+  4+ 3 send=0
...
lit               1-2  0  683    0  165 0x5555555c1b97 len= 4+  4+ 3 send=0
lit               1-2  1  684    0    6 0x5555555c1ba4 len= 4+  3+ 3 send=0
lit               1-2  2  685    0 1708 0x5555555c1bb0 len= 4+  4+ 3 send=0
lit               1-2  3  686    0  211 0x5555555c1bbd len= 4+  4+ 3 send=0
...
lit               2-3  0  707    0   36 0x5555555c1ce3 len= 4+  4+ 3 send=0
lit               2-3  1  708    0    0 0x5555555c1cf0 len= 4+  3+ 3 send=0
lit               2-3  2  709    0   53 0x5555555c1cfc len= 4+  4+ 3 send=0
lit               2-3  3  710    0  191 0x5555555c1d09 len= 4+  4+ 3 send=0
...

Here you see variants of lit with different stack-state transitions and different IP offsets (with the offset given as number of cells). The IP offset avoids the need to update the IP before the code for the primitive (see IP-update optimization); instead, the immediate argument is accessed using the offset. E.g, here we see the payload of the 0-1 variant with offsets of 0 and 3 cells (indexes 659 and 662):

0x5555555c1a4b 4 + 4 discode 
   $5555555C1A4F: mov    -0x8(%rbx),%r13
 ok
0x5555555c1a71 4 + 4 discode 
   $5555555C1A75: mov    0x10(%rbx),%r13

For the branch-to-IP variants, let’s look at ?branch:

gforth-fast --print-prims -e bye |& grep '^?branch '
?branch           1-1  0   21 1505   78 0x5555555bbc70 len= 4+ 24+ 3 send=0
?branch           2-1  0  757 1506    0 0x5555555c20f4 len= 4+ 17+ 3 send=0
?branch           2-1  1  758    0    0 0x5555555c210e len= 4+ 16+ 3 send=0
...
?branch           2-1 23  780    0    2 0x5555555c235b len= 4+ 20+ 3 send=0
?branch           1-1  0 1505    0  325 0x5555555c50b0 len= 0+ 17+ 3 send=0
?branch           2-1  0 1506    0  677 0x5555555c50c8 len= 0+ 10+ 3 send=0

Here we see that the 2-1 variant of ?branch (index 757) has the branch-to-IP variant 1506. The code for the payload of these two variants is:

0x5555555c20f4 4 + 17 discode 
   $5555555C20F8: mov    -0x8(%rbx),%rsi
   $5555555C20FC: test   %r15,%r15
   $5555555C20FF: jne    0x5555555c2109
   $5555555C2101: mov    (%rsi),%rax
   $5555555C2104: mov    %rsi,%rbx
   $5555555C2107: jmp    *%rax
 ok
0x5555555c50c8 0 + 10 discode 
   $5555555C50C8: mov    (%rbx),%rax
   $5555555C50CB: test   %r15,%r15
   $5555555C50CE: jne    0x5555555c50d2
   $5555555C50D0: jmp    *%rax

The second variant is used for optimizing short branches in the IP-update optimization by setting the IP to the branch target before the branch (see IP-update optimization).

The primitive noop has variants for several purposes:

gforth-fast --print-prims -e bye |& grep '^noop '
noop              1-1  0   10    0    3 0x5555555bbb92 len= 4+  0+ 3 send=0
noop              1-1 -24 1445    0    0 0x5555555c4dee len= 7+  0+ 3 send=0
noop              1-1 -23 1446    0    0 0x5555555c4dfa len= 7+  0+ 3 send=0
...
noop              1-1 22 1491    0    0 0x5555555c4fb1 len= 7+  0+ 3 send=0
noop              1-1 23 1492    0    0 0x5555555c4fbd len= 7+  0+ 3 send=0
noop              1-0  0 1493    0 2957 0x5555555c4fcb len= 0+  7+ 3 send=0
noop              2-0  0 1494    0  982 0x5555555c4fd9 len= 0+ 12+ 3 send=0
noop              3-0  0 1495    0  137 0x5555555c4fec len= 0+ 16+ 3 send=0
noop              0-1  0 1496    0  158 0x5555555c5003 len= 0+  8+ 3 send=0
noop              2-1  0 1497    0   89 0x5555555c5012 len= 0+ 10+ 3 send=0
noop              3-1  0 1498    0   35 0x5555555c5023 len= 0+ 14+ 3 send=0
noop              0-2  0 1499    0   40 0x5555555c5038 len= 0+ 12+ 3 send=0
noop              1-2  0 1500    0  150 0x5555555c504b len= 0+ 10+ 3 send=0
noop              3-2  0 1501    0    0 0x5555555c505c len= 0+ 13+ 3 send=0
noop              0-3  0 1502    0   15 0x5555555c5070 len= 0+ 15+ 3 send=0
noop              1-3  0 1503    0   48 0x5555555c5086 len= 0+ 15+ 3 send=0
noop              2-3  0 1504    0    5 0x5555555c509c len= 0+ 13+ 3 send=0

The variants 1445...1492 contain the IP updates that are inserted in various places when necessary, e.g., for 1445:

0x5555555c4dee 7 discode 
   $5555555C4DEE: sub    $0xc0,%rbx

These uses are not counted, so these variants are shown as having count 0.

The variants 1492...1504 contain stack-state transitions from every state to every other state that do nothing else; they are inserted between primitive variants in order to get to a stack state when there is no primitive variant for that (see Stack caching).

The shortest-path algorithm has the option of inserting any stack-state transition between two primitive variants, and selects the stack states in a way that results in a shortest code. You can see this in the counts for lit with IP offset 0: the 1-1 variant (payload length 11) is never selected, because the shortest-path algorithm apparently prefers to select a 1-0 transition (payload length 7) followed by a lit 0-1 (payload length 4) with the same total length.

We also see static superinstructions in the output of --print-prims:

$ gforth-fast --print-prims -e bye |& grep "^< [?]branch"
< ?branch         1-1  0  611  633    1 0x5555555c142a len= 4+ 28+ 3 send=0
< ?branch         1-1  0  633    0    1 0x5555555c1762 len= 4+ 21+ 3 send=0

The first variant is the regular one that branches to the address given in the immediate argument, the second is the branch-to-IP variant. We can see that both are only selected once for the code in the Gforth image.