Skip to content

Optimize String code unit search with V128 - #3816

Merged
bobzhang merged 1 commit into
mainfrom
Yu-zh/string-contains-code-unit
Aug 5, 2026
Merged

Optimize String code unit search with V128#3816
bobzhang merged 1 commit into
mainfrom
Yu-zh/string-contains-code-unit

Conversation

@Yu-zh

@Yu-zh Yu-zh commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • add builtin V128 helpers for loading and comparing eight UTF-16 code units
  • optimize String::contains_code_unit and StringView::contains_code_unit with eight-wide scans on native and wasm targets
  • retain the scalar implementation for other targets and for short/tail ranges
  • add SIMD block, view-boundary, surrogate-code-unit, and benchmark coverage

Why

Code-unit searches previously inspected one UTF-16 code unit at a time. The new intrinsic allows native and wasm backends to scan eight code units per iteration while preserving the existing behavior and public API.

Impact

Long missing or late-match searches are substantially faster on SIMD-enabled targets. String views continue to honor their exact backing-string bounds, and non-native/non-wasm targets keep the scalar path. Generated package interfaces are unchanged.

Benchmark

Isolated native ARM64 release runs comparing the scalar baseline with this implementation (lower is better):

Case Scalar baseline V128 Speedup
String, absent, n=4 7.84 ns 6.50 ns 1.21×
String, absent, n=8 8.81 ns 6.49 ns 1.36×
String, present at 0, n=8 5.77 ns 5.69 ns 1.01×
String, absent, n=256 82.26 ns 16.10 ns 5.11×
offset StringView, absent, n=256 86.38 ns 17.47 ns 4.94×

The SIMD path improves full scans substantially while keeping the immediate-match case effectively unchanged.

Validation

  • moon fmt
  • moon info (no .mbti changes)
  • moon check --target all -p builtin
  • moon test --target all -p builtin
    • wasm: 2,886 passed
    • wasm-gc: 2,886 passed
    • JavaScript: 2,874 passed
    • native: 2,844 passed
  • moon test: 6,716 passed

@coveralls

Copy link
Copy Markdown
Collaborator

Coverage Report for CI Build 5169

Coverage decreased (-0.1%) to 91.111%

Details

  • Coverage decreased (-0.1%) from the base build.
  • Patch coverage: 24 uncovered changes across 2 files (5 of 29 lines covered, 17.24%).
  • No coverage regressions found.

Uncovered Changes

File Changed Covered %
builtin/simd.mbt 17 0 0.0%
builtin/string_methods.mbt 12 5 41.67%

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 17291
Covered Lines: 15754
Line Coverage: 91.11%
Coverage Strength: 190188.93 hits per line

💛 - Coveralls

@Yu-zh
Yu-zh marked this pull request as ready for review July 16, 2026 06:56
@bobzhang
bobzhang force-pushed the Yu-zh/string-contains-code-unit branch from 697817d to 91b2850 Compare August 5, 2026 05:28
@bobzhang

bobzhang commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review sign-off (Claude)

Reviewed the diff, rebased onto latest main (which now includes the merged #3818), and re-verified. LGTM — sign-off.

Rebase

The interesting part of this rebase was the overlap with #3818, which merged first and already brought i16x8_splat, v128_load_i16x8, i16x8_eq, and u64_u16_eq_mask into builtin/simd.mbt with byte-identical definitions. After deduplication, this PR's simd.mbt delta shrinks to just its unique addition — v128_any_true — plus the header-comment merge (now naming all three scanner files). string_methods.mbt auto-merged cleanly next to #3818's find/rev_find rewrite; the bench file is new. Net diff: 3 files, +153/−6.

Correctness

  • %v128.v128_any_true is a real intrinsic (verified against the compiler's intrinsic table), and its scalar fallback body — any nonzero bit in either 64-bit half — is exactly right for an i16x8_eq mask whose lanes are all-ones or all-zeros. any_true is also the right primitive here: cheaper than a bitmask extraction when only a boolean is needed, which is why the immediate-match case stays flat while full scans get ~5×.
  • Bounds: the vector loop only loads at pos with pos + 8 <= end, and both call sites satisfy end <= str.length() (the String overload passes length(); the view path passes the view's backing-string end()). The guard start + 8 <= end keeps short ranges entirely scalar.
  • Accessors: StringView::str()/start()/end() remain valid private intrinsics on current main, so routing both String::contains_code_unit and the view method through one range-based worker is sound, and the String overload now skips the view construction entirely.
  • Semantics preserved: raw code-unit comparison, no surrogate combining — covered by the retained tests plus the new 0xD83D/0xDE00 halves of 😀.

Observation (non-blocking)

With #3818 merged, contains_code_unit could alternatively delegate to find_code_unit_from_string(...) >= 0, trading a small amount of duplicated loop structure for the cheaper any_true exit. The dedicated scanner is a deliberate and measured choice (see the n=4/n=8 rows in the PR benchmarks), so keeping it is fine — just noting the two now-parallel single-unit scanners in case a future cleanup wants to unify them.

Verification (rebased tip, moon 0.1.20260731 nightly)

  • moon check: clean; moon fmt + moon info: zero drift (no .mbti changes)
  • moon test (wasm-gc): 7007/7007
  • moon test --target all -p builtin: wasm 2937/2937, wasm-gc 2937/2937, js 2914/2914, native 2895/2895

🤖 Generated with Claude Code

@bobzhang

bobzhang commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Independent benchmark verification (Claude)

Reproduced the PR's benchmark claims on a separate machine (native ARM64, moon bench --release, moon 0.1.20260731). Both sides ran the PR's own bench file: the rebased branch for the V128 path, and a detached worktree at current main — where contains_code_unit is still the scalar loop (#3818 did not touch it) — for the baseline.

Case Scalar (main) V128 (branch) Measured PR claims
absent, n=4 7.29 ns 6.84 ns 1.07× 1.21×
absent, n=8 8.62 ns 6.99 ns 1.23× 1.36×
present@0, n=8 6.57 ns 6.09 ns 1.08× 1.01×
absent, n=16 10.49 ns 7.84 ns 1.34×
absent, n=64 24.87 ns 9.26 ns 2.69×
absent, n=256 94.96 ns 19.18 ns 4.95× 5.11×
present@end, n=256 95.86 ns 17.90 ns 5.36×
present@0, n=256 6.14 ns 6.20 ns 1.00×
StringView absent, n=256 101.05 ns 19.63 ns 5.15× 4.94×

The headline claims reproduce: ~5× on full 256-code-unit scans (the offset-view case came in slightly better than claimed), and the immediate-match case is flat at 1.00× — no regression when the needle is at position 0. The small-n rows differ from the claimed ratios by fractions of a nanosecond, within run-to-run noise at that scale, and agree in direction: modest gains, no regressions.

🤖 Generated with Claude Code

@bobzhang
bobzhang enabled auto-merge (rebase) August 5, 2026 05:33
@bobzhang
bobzhang merged commit e2afdce into main Aug 5, 2026
15 checks passed
@bobzhang
bobzhang deleted the Yu-zh/string-contains-code-unit branch August 5, 2026 05:36
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