COMP¶
The COMP cost prices a floating-point compare. Its probe, f_lte_addsub, chains
tmp = tmp - x[i] or tmp = tmp + x[i] depending on tmp >= x[i] — a compare steering
between an add and a subtract. Since add and subtract cost the same but are not free, the
subtrahend is the average of the single-ADD and single-SUB probes' latencies; the diff
below shows f_add (the SUB probe differs from it by one opcode only).
What Python code counts into COMP is described in
FLOP types.
Inner-loop diff¶
--- f_add
+++ f_lte_addsub
.L0:
ldr %d0, [%x0], #8
- fadd %d1, %d1, %d0
- str %d1, [%x1], #8
+ fneg %d1, %d0
+ fcmp %d2, %d0
+ fcsel %d0, %d1, %d0, ge
+ fadd %d2, %d2, %d0
+ str %d2, [%x1], #8
subs %x2, %x2, #1
b.ne .L0
Loop structure¶
f_add-- 2 innermost loop(s): 30 instructions, 6 instructionsf_lte_addsub-- 1 innermost loop(s): 9 instructions
The listings below are the complete compiled functions the benchmark times, raw as numba emits them (the cpython call wrappers around them are omitted -- they never run inside the timed loop). Listing lengths reflect the compiler's unrolling choices, not the probes' amount of work -- see the discussion below.
Full ASM listing: f_add
cmp x2, #1
b.lt LBB0_11
subs x8, x3, #1
b.lt LBB0_11
ldr x9, [sp, #56]
ldr x10, [sp]
and x11, x3, #0x7
and x12, x3, #0x7ffffffffffffff8
mov x13, #22377
movk x13, #35604, lsl #16
movk x13, #48906, lsl #32
movk x13, #16389, lsl #48
fmov d0, x13
b LBB0_4
LBB0_3:
subs x2, x2, #1
b.le LBB0_11
LBB0_4:
cmp x8, #7
b.hs LBB0_6
mov x13, #0
mov.16b v1, v0
b LBB0_9
LBB0_6:
mov x13, #0
add x14, x10, #32
add x15, x9, #32
mov.16b v1, v0
LBB0_7:
ldur d2, [x14, #-32]
fadd d1, d1, d2
stur d1, [x15, #-32]
ldur d2, [x14, #-24]
fadd d1, d1, d2
stur d1, [x15, #-24]
ldur d2, [x14, #-16]
fadd d1, d1, d2
stur d1, [x15, #-16]
ldur d2, [x14, #-8]
fadd d1, d1, d2
stur d1, [x15, #-8]
ldr d2, [x14]
fadd d1, d1, d2
str d1, [x15]
ldr d2, [x14, #8]
fadd d1, d1, d2
str d1, [x15, #8]
ldr d2, [x14, #16]
fadd d1, d1, d2
str d1, [x15, #16]
ldr d2, [x14, #24]
fadd d1, d1, d2
str d1, [x15, #24]
add x15, x15, #64
add x14, x14, #64
add x13, x13, #8
cmp x12, x13
b.ne LBB0_7
cbz x11, LBB0_3
LBB0_9:
lsl x14, x13, #3
add x13, x9, x14
add x14, x10, x14
mov x15, x11
LBB0_10:
ldr d2, [x14], #8
fadd d1, d1, d2
str d1, [x13], #8
subs x15, x15, #1
b.ne LBB0_10
b LBB0_3
LBB0_11:
str xzr, [x0]
mov w0, #0
ret
Full ASM listing: f_lte_addsub
cmp x2, #1
b.lt LBB0_6
cmp x3, #1
b.lt LBB0_6
ldr x8, [sp, #56]
ldr x9, [sp]
mov x10, #22377
movk x10, #35604, lsl #16
movk x10, #48906, lsl #32
movk x10, #16389, lsl #48
fmov d0, x10
LBB0_3:
mov x10, x3
mov x11, x9
mov x12, x8
mov.16b v1, v0
LBB0_4:
ldr d2, [x11], #8
fneg d3, d2
fcmp d1, d2
fcsel d2, d3, d2, ge
fadd d1, d1, d2
str d1, [x12], #8
subs x10, x10, #1
b.ne LBB0_4
subs x2, x2, #1
b.gt LBB0_3
LBB0_6:
str xzr, [x0]
mov w0, #0
ret
Discussion¶
The subtraction isolates the compare-and-select machinery — fcmp + fcsel (+ an
input-side fneg).
- What the diff shows: LLVM compiled the source-level branch branchless — instead of two
loop bodies, it negates the element up front (
fneg), compares (fcmp), selects the operand (fcsel), and always adds. Subtracting the add/sub average therefore leavesfcmp+fcsel+fnegas the priced work — a faithful stand-in for what float-comparison control flow costs in optimized code. - In the dependency chain:
fcmpreads the accumulator,fcselwaits on the flags, and thefaddwaits onfcsel, so compare and select sit squarely in the serialized chain. Thefnegruns on the freshly loaded element, off the chain, overlapping earlier work. - Loop-structure symmetry:
f_addunrolls 8×, the branchy probe does not; as on the SQRT page, both sides remain latency-bound so the subtraction holds.