Context
Follow-up to #47. With inline ldrb / strb for Buffer bracket access now in place, the bounds check is still re-loading the buffer length from memory on every single read. Disassembly of the image_conv blur inner loop (post-#47/#48):
0x100000c38 ldr w13, [x21] ; ← len = *src, reloaded every iter
0x100000c3c cmp w13, w14
0x100000c40 b.hi .oob
0x100000c44 fadd d17, d16, d8 ; next index
0x100000c48 fcvtzs w14, d17
0x100000c4c cmp w13, w14
0x100000c50 b.hi .oob
...
0x100000c68 ldr x15, [sp, #0xe8]
0x100000c6c strb w13, [x15, w14, uxtw]
0x100000c80 ldr w13, [x21] ; ← len reloaded AGAIN
The issue
x21 is &src[0].header.length — the BufferHeader.length u32 at offset 0. This is loop-invariant because:
src is bound to a const global Buffer.alloc(N)
N is a module-level const
- The only code paths that would change the length are
Buffer.concat / buf.slice assignments, none of which happen in the blur loop
Yet we see ldr w13, [x21] every single time a bounds check fires — 200M+ times per blur iteration. That's an L1-hitting load so not catastrophic, but it's dead weight and it blocks LLVM from CSE'ing the check.
Proposal
Two things, either independently useful:
-
Tag the length load with !invariant.load (LLVM metadata) when the buffer pointer is known not to be the target of concat/slice/reassign in the live range. LLVM's GVN + LICM passes will hoist it to the preheader automatically.
-
Emit the length to a SSA local at function entry for buffers declared at module scope or in non-escaping local scope, and let subsequent bracket accesses use the SSA value instead of reloading. Bypasses LLVM needing to see the invariance.
The cleaner fix is (1) — gives LLVM all the information and lets it decide. (2) is a fallback if the analysis for "safe to mark invariant" is too conservative.
Impact
Third-order compared to #49/#50 (microarchitectural, not algorithmic) but it's the reason the per-access overhead is 4 instructions instead of 2 — and with #50 unblocking autovectorization, the vector paths won't generate if the bounds check blocks them. Worth landing in the same cluster.
Scope
Any inline buffer/typed-array access emitted by #47. Also applies to arr.length reads in ordinary Array<T> bracket access.
Context
Follow-up to #47. With inline
ldrb/strbfor Buffer bracket access now in place, the bounds check is still re-loading the buffer length from memory on every single read. Disassembly of the image_conv blur inner loop (post-#47/#48):The issue
x21is&src[0].header.length— theBufferHeader.lengthu32 at offset 0. This is loop-invariant because:srcis bound to aconstglobalBuffer.alloc(N)Nis a module-level constBuffer.concat/buf.sliceassignments, none of which happen in the blur loopYet we see
ldr w13, [x21]every single time a bounds check fires — 200M+ times per blur iteration. That's an L1-hitting load so not catastrophic, but it's dead weight and it blocks LLVM from CSE'ing the check.Proposal
Two things, either independently useful:
Tag the length load with
!invariant.load(LLVM metadata) when the buffer pointer is known not to be the target of concat/slice/reassign in the live range. LLVM's GVN + LICM passes will hoist it to the preheader automatically.Emit the length to a SSA local at function entry for buffers declared at module scope or in non-escaping local scope, and let subsequent bracket accesses use the SSA value instead of reloading. Bypasses LLVM needing to see the invariance.
The cleaner fix is (1) — gives LLVM all the information and lets it decide. (2) is a fallback if the analysis for "safe to mark invariant" is too conservative.
Impact
Third-order compared to #49/#50 (microarchitectural, not algorithmic) but it's the reason the per-access overhead is 4 instructions instead of 2 — and with #50 unblocking autovectorization, the vector paths won't generate if the bounds check blocks them. Worth landing in the same cluster.
Scope
Any inline buffer/typed-array access emitted by #47. Also applies to
arr.lengthreads in ordinaryArray<T>bracket access.