Skip to content

barcodes: signed matrix words keep the encoder on one JIT specialization - #315

Merged
shreeve merged 1 commit into
mainfrom
encoder-int32
Sep 16, 2026
Merged

shreeve merged 1 commit into
mainfrom
encoder-int32

Conversation

@shreeve

@shreeve shreeve commented Sep 16, 2026

Copy link
Copy Markdown
Owner

Summary

Encode timings depended on which symbol size a process saw first. A version 1 symbol never fills a 32-bit word, so a JIT that meets it first specializes the encoder on small integers; the first larger symbol produces full words, which read as doubles from a Uint32Array, and the recompiled mixed-type code runs ~1.4x slower for the rest of the process. The reference implementation flips the same way.

The matrix is now an Int32Array. Every word use is bitwise or a popcount, both sign-agnostic, so the >>> 0 coercions and unsigned mask literals go. A full word is an ordinary int32 in every version and the encoder keeps one specialization whatever order sizes arrive in.

The README gains a Performance section with the encode and decode tables against paulmillr/qr 0.7.0 and this explanation.

Measurements

v1-then-v8 pattern, raw v8 encode, five processes (µs):

runs
before 24.8 25.0 25.4 25.3 24.7
after 16.5 16.4 16.5 16.7 16.6

Warmed on its own size: unchanged (16–17 µs both). Decode rows unchanged.

Verification

  • Encoder parity oracle: 1452 checks, 0 mismatches; decoder oracle: 192 checks, 0 mismatches
  • Package tests 33/33; bun run test:all 26 lanes, 9895 tests

Encode timings depended on which symbol size a process saw first. A
version 1 symbol fits one 32-bit word per row and never fills a word, so
a JIT meeting it first specializes the encoder on small integers; the
first larger symbol produces full words, which read as doubles from a
Uint32Array, and the recompiled mixed-type code runs about 1.4x slower
for the rest of the process (raw v8: 17 µs warmed on its own size,
25 µs warmed on v1; the reference implementation flips the same way).

The matrix is now an Int32Array. Every word use is bitwise or a
popcount, both sign-agnostic, so the `>>> 0` coercions and the unsigned
mask literals go: masks are `~(-1 << bits)` and `-1`. A full word is an
ordinary int32 in every version and the encoder keeps one
specialization whatever order sizes arrive in. Encoder oracle
byte-identical on 1452 checks; decoder oracle identical on 192.

Measured, v1-then-v8 pattern, five processes: 16.5 16.4 16.5 16.7 16.6
µs (was 24.8 25.0 25.4 25.3 24.7). Warmed on its own size: unchanged.

README gains a Performance section with the encode and decode tables
against paulmillr/qr 0.7.0 (best of three processes, standard sequence,
Bun 1.4.0, Apple M5) and the explanation above.
@shreeve
shreeve merged commit 03bf956 into main Sep 16, 2026
2 checks passed
@shreeve
shreeve deleted the encoder-int32 branch September 16, 2026 09:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant