Skip to content

DO NOT MERGE: bits pattern in unsafe_extract_byte regresses ~27x on native - #4100

Draft
bobzhang wants to merge 1 commit into
mainfrom
hongbo/bits-pattern-bitstring
Draft

DO NOT MERGE: bits pattern in unsafe_extract_byte regresses ~27x on native#4100
bobzhang wants to merge 1 commit into
mainfrom
hongbo/bits-pattern-bitstring

Conversation

@bobzhang

@bobzhang bobzhang commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

⚠️ Do not merge as-is — this regresses on every backend

Opened as a draft to record a measured negative result from the bits-pattern sweep, as the counterpart to #4099 (which holds the conversions that win). Keeping it visible so nobody re-derives it later.

What it does

builtin/bitstring.mbt has the same manual 16-bit big-endian assembly in both copies of unsafe_extract_byte — the ArrayView[Byte] one and the BytesView one:

let b0 = bs.unsafe_get(byte_index).to_uint()
let b1 = bs.unsafe_get(byte_index + 1).to_uint()
let data = (b0 << 8) | b1

This replaces both with guard! bs[byte_index:] is [u16be(data), ..]. It is correct — the full suite passes on all three backends (native 7370, wasm-gc 7454, js 7398) — and it is shorter and clearer. It is just much slower.

Measured

moon bench --release, a 4096-byte buffer read at unaligned 7-bit offsets spanning two bytes.

backend manual bits pattern
native 467 ns 12.6 µs ~27×
wasm-gc 9.99 µs 18.39 µs +84%
js 7.09 µs 9.73 µs +37%

The native figure was re-run twice to confirm (12.63 µs, 13.26 µs). Its 467 ns baseline is ~0.11 ns per iteration, so the original was being inlined and heavily optimized; the slice plus guard blocks that entirely.

Why

The pattern needs a fresh view built at the computed byte_index in order to replace only two byte reads, so view construction dominates. The same shape is why v128/simd_memory.mbt's load_u32_le/load_u64_le regressed 1.6–3.5× on wasm-gc when I tried converting them, and it is the boundary that separates this PR from the one that landed the wins:

shape result
whole view already in hand — no slice big win
slice amortized over 4+ bytes (u32/u64) clear win
slice to replace 2 byte reads (u16 at an offset) loses

What would make this mergeable

Either a cheaper way to spell "read a u16be at a computed offset without materializing a view", or an unsafe/unchecked slice that the pattern can consume. Until then the manual form is the right code here, and this PR should stay closed or draft.

🤖 Generated with Claude Code

REGRESSES - do not merge as-is. Converts the manual (b0 << 8) | b1 in
both the ArrayView[Byte] and BytesView copies of unsafe_extract_byte.

Measured: native 467ns -> 12.6us (~27x), wasm-gc +84%, js +37%. The
pattern needs a fresh slice at the computed byte_index to replace only
two byte reads, so the view construction dominates.
@coveralls

Copy link
Copy Markdown
Collaborator

Coverage Report for CI Build 6161

Coverage remained the same at 90.692%

Details

  • Coverage remained the same as the base build.
  • Patch coverage: 2 of 2 lines across 1 file are fully covered (100%).
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 17898
Covered Lines: 16232
Line Coverage: 90.69%
Coverage Strength: 331763.84 hits per line

💛 - Coveralls

Comment thread builtin/bitstring.mbt
let b1 = bs.unsafe_get(byte_index + 1).to_uint()
let data = (b0 << 8) | b1
guard! bs[byte_index:] is [u16be(data), ..]
// mask off the top bits

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

xx.get(i) is Some(y)
xx.get_view(i,j) is Some([i,j....]

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.

3 participants