Context
Surfaced by the honest_bench profile of image_conv.ts (5×5 Gaussian on 3840×2160 RGB). Perry: 2686 ms. Zig: 270 ms. Rust: 462 ms. ~10× gap.
Profile via /usr/bin/sample on the inner blur loop shows src[idx] (where src: Buffer, idx: number int-typed) lowers to:
0x1000: add w1, w21, #0x2 ; compute index
0x1004: bl 0x100019364 ; → js_buffer_get(buf, idx) : 9 instrs + 3 branches
0x1008: scvtf d0, w0 ; i32 → f64
0x100c: bl 0x10001edac ; → NaN-box-double wrapper: 30+ instrs, full frame
0x1010: fmadd d15, d14, d0, d10 ; ← the actual useful multiply-accumulate
js_buffer_get itself is:
mov x8, x0
mov w0, #0
cbz x8, ret ; null check
tbnz w1, #0x1f, ret ; negative-index check
ldr w9, [x8] ; length = *buf
cmp w1, w9
b.hs ret_zero ; oob → 0
add x8, x8, w1, uxtw
ldrb w0, [x8, #8] ; the actual load
ret
Proposal
When PropertyGet's receiver is statically typed Buffer / Uint8Array and the index is an int32 expression, lower inline:
cmp w_idx, w_len ; len loaded from buf once, hoisted out of loop
b.hs .oob_trap_or_undef
ldrb w_out, [x_base, w_idx, uxtw]
…instead of bl js_buffer_get. For hot inner loops the length load is loop-invariant so it hoists once.
Expected impact
- image_conv: 208M byte-reads per blur iter × ~40 instrs/read overhead ≈ 8.3B instructions of pure wrapper code per iteration. That matches the 6.9B cycles / 44B instructions we measured total. Removing the calls should drop the loop to ~1B instructions — projected 2686 ms → 400–600 ms, i.e. parity with Rust, possibly beating it.
- Any tight Buffer loop: crypto, image processing, binary codec, hash functions — same shape.
- Secondary: unblocks LLVM's autovectorizer — with the function calls gone it'll see a clean
u8 → zext i32 → mul → add reduction and emit NEON umlal/uaddl widening-multiply-accumulate. Another 2–4× on top.
Pairing
Pairs with the integer-specialization work (filed separately) — the scvtf + NaN-box-wrapper calls should also vanish when the index/accumulator locals are provably int32-stable. Either issue lands individually worthwhile; together is multiplicative.
Repro
benchmarks/honest_bench/workloads/3_image_convolution/perry/image_conv.ts + the looped variant at /tmp/perry_repros/image_conv_loop.ts in the session that filed this. Profile method: sample <pid> 8 -file prof.txt on the release binary, then otool -tV on the binary for disassembly.
Context
Surfaced by the
honest_benchprofile ofimage_conv.ts(5×5 Gaussian on 3840×2160 RGB). Perry: 2686 ms. Zig: 270 ms. Rust: 462 ms. ~10× gap.Profile via
/usr/bin/sampleon the inner blur loop showssrc[idx](wheresrc: Buffer,idx: numberint-typed) lowers to:js_buffer_getitself is:Proposal
When PropertyGet's receiver is statically typed
Buffer/Uint8Arrayand the index is an int32 expression, lower inline:…instead of
bl js_buffer_get. For hot inner loops the length load is loop-invariant so it hoists once.Expected impact
u8 → zext i32 → mul → addreduction and emit NEONumlal/uaddlwidening-multiply-accumulate. Another 2–4× on top.Pairing
Pairs with the integer-specialization work (filed separately) — the
scvtf+ NaN-box-wrapper calls should also vanish when the index/accumulator locals are provably int32-stable. Either issue lands individually worthwhile; together is multiplicative.Repro
benchmarks/honest_bench/workloads/3_image_convolution/perry/image_conv.ts+ the looped variant at/tmp/perry_repros/image_conv_loop.tsin the session that filed this. Profile method:sample <pid> 8 -file prof.txton the release binary, thenotool -tVon the binary for disassembly.