Context
Companion to #49. Still analyzing the image_conv hot loop post-#47/#48. The remaining bl 0x10001efa0 inside the inner kernel loop is js_value_to_double — called because KERNEL[ky+2][kx+2] returns a NaN-boxed JSValue (double-tagged), which then has to be coerced for the src[idx] * k multiply.
The pattern
const KERNEL: number[][] = [
[1, 4, 7, 4, 1],
[4, 16, 26, 16, 4],
[7, 26, 41, 26, 7],
[4, 16, 26, 16, 4],
[1, 4, 7, 4, 1],
];
const KSUM = 273;
KERNEL is const, assigned once from a literal, never written. Elements are all int32-representable (max = 41). Access pattern in the hot loop:
const krow = KERNEL[ky + 2];
const k = krow[kx + 2];
rAcc += src[idx] * k;
Currently each krow[kx+2] load goes through the generic Array.prototype.get path, which reads a NaN-boxed double from arena storage and then js_value_to_double has to unwrap it.
Proposal
For module-level const X: number[][] = [literal] (or number[], string constants, etc.) with no writers in the whole program:
- Compile-time fold the literal into a static flat table in
.rodata / .data.rel.ro.
- Detect the element type. If every element is int32-representable, emit the table as
[N x i32]. Otherwise [N x double].
- Replace
X[i][j] lookups with direct pointer arithmetic against the static table (after bounds check if the indices aren't compile-time-known).
For KERNEL[ky+2][kx+2] the emitted code becomes:
; KERNEL_FLAT = [1,4,7,4,1, 4,16,26,16,4, 7,26,41,26,7, ...] as [25 x i32]
add w_offset, w_ky, #2
mov w_row, #5
mul w_offset, w_offset, w_row
add w_offset, w_offset, w_kx
add w_offset, w_offset, #2
ldr w_k, [X_KERNEL_FLAT, w_offset, uxtw #2]
— no function call, no NaN-box unwrap, no arena pointer chase. Pairs with #49 to keep rAcc as i32 end-to-end.
Impact
Scope notes
- Only for
const bindings that no code path reassigns, modifies (push/splice/bracket-set), or reflectively probes. If in doubt, bail.
- Strings inside
const object-literals could get the same treatment (inline interned).
- Works even for
let if escape analysis proves the reference never leaves the declaring function — but const is the 99% case.
Context
Companion to #49. Still analyzing the image_conv hot loop post-#47/#48. The remaining
bl 0x10001efa0inside the inner kernel loop isjs_value_to_double— called becauseKERNEL[ky+2][kx+2]returns a NaN-boxed JSValue (double-tagged), which then has to be coerced for thesrc[idx] * kmultiply.The pattern
KERNELisconst, assigned once from a literal, never written. Elements are all int32-representable (max = 41). Access pattern in the hot loop:Currently each
krow[kx+2]load goes through the generic Array.prototype.get path, which reads a NaN-boxeddoublefrom arena storage and thenjs_value_to_doublehas to unwrap it.Proposal
For module-level
const X: number[][] = [literal](ornumber[], string constants, etc.) with no writers in the whole program:.rodata/.data.rel.ro.[N x i32]. Otherwise[N x double].X[i][j]lookups with direct pointer arithmetic against the static table (after bounds check if the indices aren't compile-time-known).For
KERNEL[ky+2][kx+2]the emitted code becomes:— no function call, no NaN-box unwrap, no arena pointer chase. Pairs with #49 to keep
rAccas i32 end-to-end.Impact
blcalls per iteration (25 kernel lookups × 8.3M pixels). Combined with perf: keep int-stable accumulator as i32 through fp-divide + |0 rounding (the image-conv kernel pattern) #49, projected 2142 ms → 400–500 ms range, finally parity with Rust.Scope notes
constbindings that no code path reassigns, modifies (push/splice/bracket-set), or reflectively probes. If in doubt, bail.constobject-literals could get the same treatment (inline interned).letif escape analysis proves the reference never leaves the declaring function — butconstis the 99% case.