Skip to content

perf: inline Buffer/Uint8Array bracket-read when statically typed (9 instr + bl → 1 instr) #47

Description

@proggeramlug

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions