镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

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

Closed

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.

Activity

  1. proggeramlug commented on Apr 16, 2026

    @proggeramlug
    ContributorAuthor

    This was implemented in v0.5.39 (Int32-stable local specialization). The full pipeline is in place: collect_integer_let_ids analysis, is_int32_producing_expr/can_lower_expr_as_i32 checks, and lower_expr_as_i32 emission — locals that are provably int32-stable stay in i32 throughout their live range, with NaN-boxing only at escape boundaries. Later versions (v0.5.50–v0.5.56) extended this further with toint32_fast for known-finite bitwise operands, clamp-function inlining via smin/smax intrinsics, and i32-native bitwise ops.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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