Skip to content

perf: integer specialization for loop-stable int32 locals — skip scvtf + NaN-box round-trip #48

Description

@proggeramlug

Context

Companion to #47. honest_bench image_conv profile of Perry v0.5.30 shows two back-to-back bl calls per byte read in the inner blur loop: one for the buffer read (#47), one for a NaN-box wrapper. This issue is the second call.

At 0x100c in the profiled binary:

scvtf d0, w0              ; i32 result from buffer_get → f64
bl    0x10001edac         ; NaN-box wrapper — full frame, tag-table setup, check, return

The wrapper function starts:

sub   sp, sp, #0x80
stp   x26, x25, [sp, #0x30]
...                       ; 5× stp register spills
mov   x20, #0x1;  movk  x20, #0x7ffc, lsl #48   ; TAG_UNDEFINED
mov   x21, #0x2;  movk  x21, #0x7ffc, lsl #48   ; TAG_NULL
mov   x22, #-0x3; movk  x22, #0x8003, lsl #48
mov   x23, #0x7fff000000000000                  ; STRING_TAG
mov   w24, #0x7ffe0000                          ; INT32_TAG
mov   x25, #0x7ffd000000000000                  ; POINTER_TAG
fmov  x19, d0
cmp   x19, x20                                   ; dispatch by tag
...

— i.e. it's a generic "coerce any JSValue to double" that sets up the full tag table to handle every possible incoming tag. When called right after scvtf it's doing pointless work: the incoming value is known to be a freshly-converted double from a known-int32 source.

Proposal

Type inference already tracks stability for some locals. Extend it (or add a loop-local pass) to mark a number as int32-stable when:

For such locals, the codegen emits i32 throughout its live range: loads/stores stay as ldr w/str w, arithmetic stays as mul w/add w, and NaN-boxing happens only at the escape boundary (store to heap, argument pass to a generic callee, print, etc.).

Expected impact

Stacks on top of #47. With #47 already in place, the blur loop would be:

ldrb w0, [x_src, w_idx, uxtw]     ; from #47
mul  w0, w0, w_k                  ; keep i32
add  w_acc, w_acc, w0

— instead of ldrb → scvtf → bl nan_box → fmadd. Removes the second bl entirely (the profile showed it taking more samples than the first one — ~30+ instrs vs js_buffer_get's 9).

On the image_conv workload with #47 already applied, projected additional ~2× speedup. Combined with #47's autovectorization unblock, lands Perry ahead of Rust on tight compute loops.

Scope

  • Pixel-heavy code: image convolution, resize, colorspace conversion, compositing.
  • Hash loops: FNV, MurmurHash, xxhash (the honest_bench/…/perry/image_conv.ts uses a hand-rolled imul32 that would benefit).
  • Byte parsers: base64, UTF-8 validators, varint.

Counter-shape to be careful of

Locals that look int32 but occasionally hold NaN or Infinity (e.g. const n = parseInt(userInput) where userInput might be "abc") must not be specialized. The analysis has to trace the flow and bail on any edge where a non-int32 can enter. The compile-time type (number in TS) is not enough; actual use has to be checked.

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