Many primitives do not need an up-to-date IP. The IP-update
optimization delays updating the IP until an up-to-date IP is needed.
For the squared example, the result is:
<squared> dup 1->2 $7F9F4AC739F7: mov %r13,%r15 <squared+$8> * 2->1 $7F9F4AC739FA: imul %r15,%r13 <squared+$10> ;s 1->1 $7F9F4AC739FE: mov (%r14),%rbx $7F9F4AC73A01: add $0x8,%r14 $7F9F4AC73A05: mov (%rbx),%rax $7F9F4AC73A08: jmp *%rax
In this case none of the primitives needs an up-to-date IP; the first
two don’t access IP at all, while ;s writes to IP itself and
therefore does not need an up-to-date IP, either.
If an up-to-date IP is needed, a code fragment is inserted that adds the appropriate number to the IP. There is only a limited amount of such fragments, but that’s good enough in most cases; and if it’s not enough one can insert several such fragments.
An up-to-date IP is typically needed for primitives that access immediate arguments, and before inserting a NEXT in front of a non-relocatable primitive, and at a control-flow join. The IP-update optimization also reduces the need for an up-to-date IP for immediate arguments by providing variants of the most common primitives with immediate arguments that work with an outdated IP (up to some limit).
One might think that the lower number of instructions is the main benefit of this optimization, and this benefit exists, but is relatively small (speedups factors around 1.1 on high-performance hardware on call-dominated benchmarks). But the major benefit on modern high-performance hardware is that in loop-dominated programs the critical data-dependence chain is shortened, resulting in a speedup by a factor of 2 or more for these programs on recent high-performance cores, when the following suboptimization is also performed.
Without IP update optimization branch and friends use a load
(3-5 cycles latency) to get the target address. The IP-update
optimization changes the IP with an add (at the beginning of the
branch code) to point to the target, so the load is now unnecessary.
This can be seen in the mymin example:
gforth-fast --opt-ip-updates=0 gforth-fast <mymin> 2dup 1->3 <mymin> 2dup 1->3 add $0x8,%rbx mov 0x8(%r10),%r15 mov 0x8(%r10),%r15 mov %r13,%r9 mov %r13,%r9 <mymin+$8> < 3->2 <mymin+$8> < 3->2 add $0x8,%rbx cmp %r9,%r15 cmp %r9,%r15 setl %r15b setl %r15b movzbl %r15b,%r15d movzbl %r15b,%r15d neg %r15 neg %r15 <mymin+$10> ?branch 2->1 <mymin+$10> ?branch 2->1 <mymin+$18> <mymin+$38> <mymin+$18> <mymin+$38> add $0x10,%rbx add $0x38,%rbx mov -0x8(%rbx),%rsi mov (%rbx),%rax test %r15,%r15 test %r15,%r15 jne <to the next add> jne <to the next mov> mov (%rsi),%rax mov %rsi,%rbx jmp *%rax jmp *%rax <mymin+$20> drop 1->1 <mymin+$20> drop 1->1 add $0x8,%rbx mov 0x8(%r10),%r13 mov 0x8(%r10),%r13 add $0x8,%r10 add $0x8,%r10 <mymin+$28> branch 1->1 <mymin+$28> branch 1->1 <mymin+$30> <mymin+$40> <mymin+$30> <mymin+$40> add $0x10,%rbx add $0x8,%rbx mov -0x8(%rbx),%rbx mov (%rbx),%rax mov (%rbx),%rax jmp *%rax jmp *%rax <mymin+$38> nip 1->1 <mymin+$38> nip 1->1 add $0x8,%rbx add $0x8,%r10 add $0x8,%r10 add $0x8,%rbx <mymin+$40> ;s 1->1 <mymin+$40> ;s 1->1 mov (%r14),%rbx mov (%r14),%rbx add $0x8,%r14 add $0x8,%r14 mov (%rbx),%rax mov (%rbx),%rax jmp *%rax jmp *%rax
The IP update in the nip code is due to having a control-flow
join (then) after the nip. The IP is dead at that
point, but Gforth’s code generator does not look ahead far enough to
know that and leave it away.
One other thing you may notice is that the static superinstruction for
< ?branch is not used. That’s because stack caching results in
a shorter code sequence than the static superinstruction (which with
the current limitations would require the 1->1 variant of
2dup.
The IP-update optimization is controlled with --opt-ip-updates=n, where n=0 turns off the optimization completely, other values are described in more detail in the See option description.
You can read more about the IP-update optimization in: M. Anton Ertl, Bernd Paysan, The Performance Effects of Virtual-Machine Instruction Pointer Updates, In 38th European Conference on Object-Oriented Programming (ECOOP 2024).