Context
Follow-up to #47 + #48. Post-fix image_conv wall time is 2142 ms vs Zig 246 ms (8.7×). Disassembly shows the remaining hot inner loop is still fp:
0x1008 fmadd d3, d9, d0, d8 ; rAcc += byte * kernel, in doubles
0x1010 ldr d4, [sp, #0xe8] ; index reload from stack
0x1014 fadd d4, d4, d0 ; index += 1.0 (!)
0x1028 fcvtzs x8, d0 ; fp → int for bounds check
The pattern that currently taints
let rAcc = 0;
for (...) {
rAcc += src[idx] * k; // all operands fit in i16;
// sum of 25 fits in i18
}
dst[out] = clampU8((rAcc / KSUM) | 0); // ← fp divide + truncate
Perry's int-spec (landed in #48) correctly says "rAcc can't be int because it flows into /". That's true for rAcc / KSUM, which is fp. But the | 0 right after the divide means the fp result is immediately truncated back to int — so fp is only a rounding step, not a semantic that needs i32→f64 conversion on every accumulate.
Proposal
Extend the int-stability analysis to recognize this exit pattern:
acc += int_expr (n times)
result = (acc / C) | 0 where C is an int32 constant
When acc is otherwise int-stable (feeds only this pattern), keep acc as i32 inside the loop and emit the divide/round at the exit:
sdiv w_result, w_acc, w_C ; or a mul-by-reciprocal reduction
…instead of:
scvtf d_acc, w_acc
fdiv d_acc, d_acc, d_C
fcvtzs w_result, d_acc
Impact
Image convolution, box blur, Gaussian blur, matrix-weighted kernels, colorspace conversion, IIR filters, weighted histogram, fixed-point DSP — every signal processing idiom that uses integer weights with a final rounding divide. For honest_bench/workloads/3_image_convolution/perry/image_conv.ts specifically, would remove the per-sample fmadd d3, d9, d0, d8 round-trip inside the 25-wide kernel accumulation.
Projected for image_conv: 2142 ms → sub-1000 ms (3-4× of Zig's 246 ms), in combination with #[const-array-inlining follow-up filed separately].
Edge cases to bail on
- Divide by a non-constant
- Divide by 0 (wouldn't matter for
| 0 but unclear semantics)
- The accumulator's loop sum can exceed
i32::MAX — analysis has to bound the worst-case sum (at compile time, from the known loop trip count + operand bounds, or conservatively leave it as fp)
Repro for verification
benchmarks/honest_bench/workloads/3_image_convolution/perry/image_conv.ts — the blur()-equivalent module body.
Context
Follow-up to #47 + #48. Post-fix image_conv wall time is 2142 ms vs Zig 246 ms (8.7×). Disassembly shows the remaining hot inner loop is still fp:
The pattern that currently taints
Perry's int-spec (landed in #48) correctly says "
rAcccan't be int because it flows into/". That's true forrAcc / KSUM, which is fp. But the| 0right after the divide means the fp result is immediately truncated back to int — so fp is only a rounding step, not a semantic that needs i32→f64 conversion on every accumulate.Proposal
Extend the int-stability analysis to recognize this exit pattern:
When
accis otherwise int-stable (feeds only this pattern), keepaccas i32 inside the loop and emit the divide/round at the exit:…instead of:
Impact
Image convolution, box blur, Gaussian blur, matrix-weighted kernels, colorspace conversion, IIR filters, weighted histogram, fixed-point DSP — every signal processing idiom that uses integer weights with a final rounding divide. For
honest_bench/workloads/3_image_convolution/perry/image_conv.tsspecifically, would remove the per-samplefmadd d3, d9, d0, d8round-trip inside the 25-wide kernel accumulation.Projected for image_conv: 2142 ms → sub-1000 ms (3-4× of Zig's 246 ms), in combination with #[const-array-inlining follow-up filed separately].
Edge cases to bail on
| 0but unclear semantics)i32::MAX— analysis has to bound the worst-case sum (at compile time, from the known loop trip count + operand bounds, or conservatively leave it as fp)Repro for verification
benchmarks/honest_bench/workloads/3_image_convolution/perry/image_conv.ts— theblur()-equivalent module body.