← board

Track O: float expression temporaries in registers

Where the 4x goes

The x86-64 float binop path is an accumulator machine: each IR_BINOP loads operands from stack slots, computes in xmm0/xmm1, and spills the result back (EmitFloatSpill386-style patterns on x86-64 too). The mandelbrot inner loop (zrzr - zizi + cr etc.) round-trips every temporary through memory; FPC keeps the whole escape iteration in registers.

Recon (2026-07-15 morning — root confirmed)

The x86-64 VALUE MODEL carries a Double as raw bits in RAX. A float IR_BINOP therefore emits: eval left -> rax, push rax; eval right -> rax, mov rcx<-pop; movq xmm1, rax; movq xmm0, rcx; <op>; movq rax, xmm0. Three GPR<->XMM transfers plus a stack round-trip PER OPERATION — the whole 4.2x against FPC's xmm-resident code. The narrow "fuse within one tree" idea still pays, but the honest fix is an xmm-resident float accumulator (xmm0 = the float accumulator; nested left operands spill to the stack as today, but as MOVSD spills, no GPR transit), which touches every float consumer: binops, compares, call args/returns, stores, writeln. Sized as a MULTI-SESSION Track O arc — do not start it as a night-tail.

User constraints (2026-07-15)

Shape (per the regcall/residency precedents)

Re-measured 2026-07-18 night (fable-O) — gap HALVED since opening, arc still valid

Same box, same checksum (74607393270), quiet machine, hyperfine w2/r7:

build time vs FPC -O2
pxx -O2 1.399 s ± 0.009 4.2x (unchanged — ticket baseline)
pxx -O3 0.664 s ± 0.005 1.97x
FPC -O2 0.337 s ± 0.003 1.00x

Re-confirmed 2026-08-15 from a SECOND workload (Track B, transcendental kernels)

Independent measurement, different code, same root — recorded because it puts a clean isolated number on the value model and separates it from two things that were being blamed instead.

lib/rtl/math.pas's new fast Sin vs the identical algorithm compiled by gcc -O2 -mno-fma (same instruction set, no FMA, so this is codegen alone), 1M calls:

variant time what it isolates
pxx Sin, as shipped 131 ms
pxx, only the needed kernel (skip the wasted cos) 77 ms 1.5x — algorithmic, Track B's to fix
pxx, hand-inlined into ONE function, zero calls 65 ms 1.2x — call overhead
gcc, same source, same ISA 9 ms 7.2x — the value model
glibc sin 7 ms 1.3x — glibc's extra fast paths

Call overhead is 1.2x here, not the story. That matters because [[feature-opt-inline-float-and-record-returning-leaves]] was filed the same day off a 3.8x inlining measurement — but that was on the double-double kernels, which are ten-op functions called 26 times per evaluation. On ordinary plain-double code the inliner is worth ~20%, and this ticket is worth 7x.

Disassembly of the hand-inlined pxx function (--map, then objdump on the raw image — pxx ELFs carry no section headers):

811 instructions total
 80   real float arithmetic
316   movq xmm<->GP moves        <- gcc emits ZERO for the same source
125   stack-slot references      <- gcc: 8
 61   instructions in gcc's whole function

10 instructions emitted per one of arithmetic. The signature pattern, verbatim:

mulsd  xmm0,xmm1
movq   rax,xmm0                    ; result out to the integer file
movq   xmm0,rax                    ; ...and straight back, a pure no-op
movsd  QWORD PTR [rbp-0xb8],xmm0   ; spill
movsd  xmm0,QWORD PTR [rbp-0xb8]   ; reload, next instruction

28 of those movq r,xmm ; movq xmm,r pairs are provably dead in this one function, and 12 store/reload pairs touch a slot that was live in a register.

Two notes for whoever takes this:

Still the user's call on priority (parked at 20 on 2026-07-19, "general code speed over float"). Recording the number, not arguing the rank: the scope has widened since — every frontend's float path, lib/rtl math, and the ESP/sensor work all sit on this emission.