Skip to content

⚡ feat: Add SIMD-accelerated search package - #237

Merged
ReneWerner87 merged 10 commits into
masterfrom
claude/simd-utils-integration-b2ls05
Jul 22, 2026
Merged

⚡ feat: Add SIMD-accelerated search package#237
ReneWerner87 merged 10 commits into
masterfrom
claude/simd-utils-integration-b2ls05

Conversation

@gaby

@gaby gaby commented Jul 21, 2026

Copy link
Copy Markdown
Member

Summary

Ports the simd package from coregx/coregex (MIT, license preserved in simd/LICENSE and per-file headers) as a new exported subpackage, without taking coregex on as a dependency: AVX2 assembly kernels on amd64 processing 32 bytes per iteration, with portable fallbacks built on our own swar package everywhere else. The top-level IsASCII, IndexAny2, and IndexAny3 dispatch into it automatically for inputs of simd.MinLen (32) bytes and up on amd64, via a zero-copy byteSeq view. The only new module dependency is golang.org/x/sys (CPU feature detection).

Fixes #234

New API (package simd)

Tier Functions
AVX2 kernels + SWAR fallback Memchr2, Memchr3 (multi-needle), MemchrPair (fixed-distance pair, substring prefilter), MemchrDigit/MemchrDigitAt, MemchrWord/MemchrNotWord, IsASCII, Memmem (rare-byte prefiltered substring search)
SWAR only (8 B/iter) FirstNonASCII, CountNonASCII
Scalar (no vector form exists) MemchrInTable, MemchrNotInTable
Support SelectRareBytes, ByteRank, RareByteInfo, Accelerated(), MinLen

There is deliberately no single-needle Memchr: bytes.IndexByte is already vector-accelerated by the Go runtime and beat the upstream kernel at every size.

Adaptations from upstream

  • Fixed an AVX-SSE transition bug: upstream kernels mix legacy-SSE MOVD/MOVQ with VEX instructions, a ~150 ns/call stall on Intel (Memchr2 512B: 209 ns → 16 ns after switching to VMOVD/VMOVQ).
  • Vectorized the kernel tails: the scalar byte-at-a-time tail loops were replaced with one overlapping vector at the buffer end (a 63-byte miss cost 28.1 ns vs 7.7 ns at 64B; now both ~7.1 ns).
  • Bounded Memmem's worst case: the prefilter loops bail to bytes.Index after 16 failed candidate verifications, so the adversarial shape (rare byte everywhere + long almost-matching needle) stays within ~2% of stdlib instead of degrading to O(n·m) (measured 47 ms → 1.67 ms vs stdlib's 1.64 ms on a 1 MiB haystack). Below its measured 128-byte crossover Memmem delegates to bytes.Index outright.
  • Fallbacks rebuilt on swar: one source of truth for the bit tricks; this also gives the digit/word-class scans and FirstNonASCII/CountNonASCII word-wide fallbacks on non-amd64 (arm64) instead of byte-at-a-time loops.
  • The byte-frequency table is unexported behind ByteRank so callers can't mutate the ranking the prefilter depends on.

Benchmarks

Environment: linux/amd64, Intel Xeon @ 2.10 GHz (AVX2), Go 1.25. Full tables in the README; per-commit charts will pick the new benchmarks up.

Top-level functions vs master (benchstat -count=10, sec/op; sub-32B inputs keep the SWAR path and are unchanged within noise):

                        master        this PR
IndexAny2/32B          8.232n ± 3%    5.106n ± 4%   -37.98% (p=0.000)
IndexAny2/64B         18.435n ±26%    6.053n ± 2%   -67.17% (p=0.000)
IndexAny2/512B         97.41n ± 5%    16.89n ± 2%   -82.67% (p=0.000)
IndexAny3/32B         11.395n ±14%    5.568n ± 2%   -51.14% (p=0.000)
IndexAny3/64B         18.720n ± 1%    6.679n ± 1%   -64.32% (p=0.000)
IndexAny3/512B        146.05n ±28%    16.86n ±10%   -88.45% (p=0.000)
IsASCII/32B            5.819n ± 3%    5.278n ±36%    -9.30% (p=0.023)
IsASCII/64B            7.221n ± 3%    6.425n ± 4%   -11.01% (p=0.000)
IsASCII/512B           34.05n ± 4%    12.61n ± 3%   -62.99% (p=0.000)
geomean (all sizes)    19.14n         15.20n        -20.60%

Package simd vs the idiomatic alternative (stdlib where one exists, scalar loop otherwise; worst-case full-scan misses, ns/op):

Benchmark 32B 64B 512B 4096B
Memchr2 vs bytes.ContainsAny 6.7 / 42.2 7.2 / 56.6 20.9 / 372 140 / 3001
Memchr3 vs bytes.ContainsAny 7.2 / 33.8 8.3 / 56.5 22.9 / 377 142 / 2877
MemchrPair vs scalar 20.7 / 21.5 10.4 / 40.9 21.5 / 338 117 / 2640
MemchrDigit vs scalar 6.3 / 12.6 6.9 / 22.7 24.5 / 164 177 / 1283
MemchrNotWord vs scalar 8.1 / 30.5 10.4 / 49.7 41.0 / 515 304 / 3191
IsASCII vs SWAR 5.1 / 8.7 6.4 / 12.3 14.6 / 85.7 99.0 / 704

Memmem vs bytes.Index — the 96B/128B rows bracket the crossover encoded in memmemMinHaystack, and the adversarial row pins the miss-budget fallback:

Size Memmem bytes.Index
96B 54.0 ns 57.1 ns
128B 25.5 ns 65.3 ns
512B 34.7 ns 252 ns
4096B 132 ns 1958 ns
Adversarial (1 MiB, 1000B needle) 1.67 ms 1.65 ms

Testing

  • Sweep tests compare every exported function and its portable fallback against scalar references across all dispatch boundaries (sub-word, sub-vector, vector+tail, crossover sizes), including every planted match position.
  • Five fuzz targets differential-test against stdlib, including the SWAR pair fallback and the Memmem prefilter internals directly (so they stay covered on runners without AVX2); ~3M execs clean.
  • GOFIBER_SIMD_REQUIRE_AVX2=1 makes Test_Accelerated fail if the assembly kernels are not actually active, guarding against silently fallback-only CI.
  • Full suite passes with -race -shuffle=on; builds verified on amd64, arm64, 386, s390x, and wasm; golangci-lint clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_017QqEx79JhskHN7sRz5Pj5R

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Added SIMD-accelerated byte/substring search and validation (including ASCII checks and character classification such as digits and word characters).
    • Improved substring searching with an optimized rare-byte prefilter on supported amd64 systems, with safe portable fallbacks elsewhere.
  • Documentation

    • Updated the published benchmark catalog with new SIMD results.
    • Added a section detailing SIMD-accelerated search behavior and coverage.
  • Tests

    • Added dispatch correctness tests, extensive SIMD test suites, and fuzz/adversarial coverage.

claude added 3 commits July 21, 2026 14:14
Port the simd package from github.com/coregx/coregex (MIT License,
preserved in simd/LICENSE and per-file headers) as a new exported
subpackage: AVX2 assembly kernels on amd64 processing 32 bytes per
iteration, with portable SWAR fallbacks everywhere else.

Exported API: Memchr2/Memchr3 (multi-needle scan), MemchrPair
(fixed-distance pair scan), MemchrDigit/MemchrDigitAt,
MemchrWord/MemchrNotWord, MemchrInTable/MemchrNotInTable, IsASCII/
FirstNonASCII/CountNonASCII, Memmem with rare-byte prefiltering
(SelectRareBytes/ByteRank/ByteFrequencies), and Accelerated().

Adaptations from upstream:
- Replace legacy-SSE MOVD/MOVQ register moves in the kernels with
  VEX-encoded VMOVD/VMOVQ: the originals dirty the YMM upper state and
  trigger AVX-SSE transition stalls on Intel CPUs, measured as a
  constant ~150ns per call (Memchr2 at 512B: 209ns before, 16ns after).
- Drop the single-byte Memchr kernel: bytes.IndexByte is already
  vector-accelerated by the Go runtime and beat it at every size, so
  single-byte search delegates to the stdlib instead.
- Memmem uses the SIMD path only with AVX2 and 128+ byte haystacks (the
  measured crossover) and otherwise delegates to bytes.Index, so it is
  never meaningfully slower than the stdlib.
- CPU capability comes from a self-contained CPUID/XGETBV probe.

Top-level IsASCII, IndexAny2, and IndexAny3 now dispatch to the AVX2
kernels for inputs of 32+ bytes on amd64 via a zero-copy byteSeq view
(unsafeconv.Bytes); benchstat -count=10 against the previous SWAR word
loops shows -35% to -88% ns/op at 32-512B on an AVX2 Xeon, with
sub-32-byte inputs keeping the existing SWAR paths. Fallback paths are
compared against scalar references across all dispatch boundaries by
sweep tests and five fuzz targets.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017QqEx79JhskHN7sRz5Pj5R
Replace the hand-rolled CPUID/XGETBV probe with the official
golang.org/x/sys/cpu package, which performs the same CPU and OS
YMM-state checks and is maintained by the Go team.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017QqEx79JhskHN7sRz5Pj5R
Correctness/robustness:
- Memmem: cap failed candidate verifications at 16 and fall back to
  bytes.Index from the first unchecked position, removing the O(n*m)
  adversarial worst case (1 MiB of 'z' with a 1000-byte almost-matching
  needle: 47ms -> 1.7ms, now within 2% of bytes.Index; see
  Benchmark_Memmem_Adversarial). Docs updated to state the bounded
  fallback instead of overclaiming.
- AVX2 kernels: replace the scalar byte-at-a-time tail loops with one
  overlapping vector at the buffer end in all seven kernels (safe because
  already-scanned lanes are known non-matching, the same argument as the
  SWAR overlapping word at n-8). A 63-byte Memchr2 miss drops from
  28.1ns to 7.1ns, matching the 64-byte cost. Scalar loops remain only
  for sub-MinLen direct calls, which the Go dispatch never makes.
- Fix the kernel header comments that misdescribed the ABI0 frame
  (byte params are packed 1-byte slots at +24/+25/+26, not overlapping
  8-byte slots).

Single sources of truth:
- Export simd.MinLen (32) and use it in every dispatch gate, including
  the top-level IsASCII/IndexAny2/IndexAny3 prologues, so the kernel
  crossover cannot drift between packages.
- Rebuild all portable fallbacks on the swar package primitives
  (Broadcast/Load8/ZeroLanes/FirstLane/MatchRangeMask/MatchByteMask)
  instead of hand-rolled constants and formulas; this also upgrades the
  digit/word-class fallbacks and FirstNonASCII/CountNonASCII from
  byte-at-a-time loops to word-wide SWAR scans on non-amd64.
- Unexport the byte-frequency table behind ByteRank so callers cannot
  mutate the ranking Memmem's prefilter depends on.

Docs and evidence:
- Package doc and README now state the acceleration tier of every
  function (AVX2+SWAR, SWAR-only, or scalar) instead of a blanket claim.
- Benchmark_Memmem gains 96/128/192-byte points bracketing the 128-byte
  bytes.Index crossover that memmemMinHaystack encodes, and an
  adversarial benchmark pinning the miss-budget fallback.
- The arm64 catalog's Search/ASCII sections now point at the amd64 simd
  numbers below.

Test coverage:
- Direct tests and fuzzing for memmemPaired/memmemSingle (previously
  reachable only with AVX2) including the fallback handover, and direct
  fuzzing of memchrPairGeneric (previously only reachable below the
  dispatch length on AVX2 hosts).
- Test_Accelerated can be made to fail on runners that should have AVX2
  via GOFIBER_SIMD_REQUIRE_AVX2=1, so fallback-only CI runs cannot
  silently green-light the assembly surface.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017QqEx79JhskHN7sRz5Pj5R
@gaby
gaby requested a review from a team as a code owner July 21, 2026 14:29
@gaby
gaby requested review from ReneWerner87, efectn and sixcolors and removed request for a team July 21, 2026 14:29
@gaby gaby changed the title Add SIMD-accelerated search package adapted from coregex ⚡ feat: Add SIMD-accelerated search package Jul 21, 2026
@gaby
gaby marked this pull request as draft July 21, 2026 14:29
@codecov

codecov Bot commented Jul 21, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.11%. Comparing base (6488cc3) to head (3e07bbb).

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #237      +/-   ##
==========================================
+ Coverage   89.38%   91.11%   +1.73%     
==========================================
  Files          17       28      +11     
  Lines        1432     1756     +324     
==========================================
+ Hits         1280     1600     +320     
- Misses        132      135       +3     
- Partials       20       21       +1     
Flag Coverage Δ
unittests 91.11% <100.00%> (+1.73%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

CI's revive flags unexported functions that differ from an exported
name only by capitalization (IsASCII/isASCII, Memchr2/memchr2, ...).
Rename the per-arch dispatch hooks with an Impl suffix; no behavior
change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017QqEx79JhskHN7sRz5Pj5R
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@gaby, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 30 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a86ed17e-5a17-4ac5-8e91-ab2b53820763

📥 Commits

Reviewing files that changed from the base of the PR and between a550a21 and 92b3894.

📒 Files selected for processing (7)
  • README.md
  • simd/bench_test.go
  • simd/memchr.go
  • simd/memchr_amd64.go
  • simd/memchr_amd64.s
  • simd/simd.go
  • simd/simd_test.go
📝 Walkthrough

Walkthrough

This PR adds portable SWAR and amd64 AVX2 byte-search primitives, integrates accelerated paths into IsASCII and IndexAny2/3, and adds tests, fuzzing, benchmarks, licensing, and documentation.

Changes

SIMD search package

Layer / File(s) Summary
Portable primitives and search contracts
simd/*.go, simd/*_other.go
Adds portable ASCII, byte-search, character-class, digit-search, rare-byte selection, and Memmem implementations with non-amd64 fallbacks.
AVX2 kernels and dispatch
simd/*_amd64.go, simd/*.s, simd/cpu_*.go
Adds AVX2 dispatch and assembly kernels for ASCII, multi-byte search, word classification, and digit scanning.
Existing helper integration
ascii.go, search.go, internal/unsafeconv/unsafeconv.go, search_test.go
Routes eligible long inputs through SIMD helpers and adds unsafe byte views plus named-type dispatch tests.
Validation, benchmarks, and documentation
simd/*_test.go, simd/LICENSE, README.md, swar/swar.go
Adds unit, fuzz, adversarial, and benchmark coverage, updates licensing and SWAR contracts, and documents SIMD behavior and benchmark results.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant ExistingHelpers
  participant UnsafeBytes
  participant SIMD
  participant AVX2
  participant SWAR
  Caller->>ExistingHelpers: call IsASCII or IndexAny2/3
  ExistingHelpers->>UnsafeBytes: convert string-like input
  ExistingHelpers->>SIMD: dispatch eligible byte input
  SIMD->>AVX2: use AVX2 when available and input is at least MinLen
  SIMD->>SWAR: use portable fallback otherwise
  AVX2-->>ExistingHelpers: return search or ASCII result
  SWAR-->>ExistingHelpers: return search or ASCII result
Loading

Possibly related PRs

Suggested labels: 🧹 Updates

Suggested reviewers: sixcolors, efectn, renewerner87

Poem

Hops through bytes, the bunny beams,
AVX2 chases searchy dreams.
SWAR waits where fallbacks lie,
Tests guard every trail nearby. 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 56.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately reflects the PR's main addition: a SIMD-accelerated search package.
Linked Issues check ✅ Passed The PR delivers the core SIMD search objective with AVX2 dispatch, SWAR fallbacks, x/sys/cpu detection, and dispatch-boundary tests.
Out of Scope Changes check ✅ Passed The added docs, tests, benchmarks, and helper code all support the SIMD search package and do not appear unrelated.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/simd-utils-integration-b2ls05

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@simd/ascii_amd64.go`:
- Line 16: Suppress the revive confusing-naming lint for the intentional private
dispatchers: apply the same suppression to isASCII in simd/ascii_amd64.go:16-16,
and to both memchrWord and memchrNotWord in simd/memchr_class_amd64.go:20-33, or
add an equivalent revive exclusion covering these symbols/files. Preserve the
existing names and exported wrapper behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4452bcbf-5e88-40bc-b084-057e310f5f68

📥 Commits

Reviewing files that changed from the base of the PR and between 6488cc3 and 74a4f97.

⛔ Files ignored due to path filters (2)
  • go.mod is excluded by !**/*.mod
  • go.sum is excluded by !**/*.sum, !**/*.sum
📒 Files selected for processing (30)
  • README.md
  • ascii.go
  • internal/unsafeconv/unsafeconv.go
  • search.go
  • search_test.go
  • simd/LICENSE
  • simd/ascii.go
  • simd/ascii_amd64.go
  • simd/ascii_amd64.s
  • simd/ascii_other.go
  • simd/bench_test.go
  • simd/byte_frequencies.go
  • simd/cpu_amd64.go
  • simd/cpu_other.go
  • simd/fuzz_test.go
  • simd/memchr.go
  • simd/memchr_amd64.go
  • simd/memchr_amd64.s
  • simd/memchr_class.go
  • simd/memchr_class_amd64.go
  • simd/memchr_class_amd64.s
  • simd/memchr_class_other.go
  • simd/memchr_digit.go
  • simd/memchr_digit_amd64.go
  • simd/memchr_digit_amd64.s
  • simd/memchr_digit_other.go
  • simd/memchr_other.go
  • simd/memmem.go
  • simd/simd.go
  • simd/simd_test.go
📜 Review details
⏰ Context from checks skipped due to timeout. (4)
  • GitHub Check: benchmark / benchmark
  • GitHub Check: Build (1.25.x, windows-latest)
  • GitHub Check: Build (1.25.x, macos-latest)
  • GitHub Check: Build (1.26.x, windows-latest)
🧰 Additional context used
🪛 GitHub Check: lint / lint
simd/ascii_amd64.go

[failure] 16-16:
confusing-naming: Method 'isASCII' differs only by capitalization to function 'IsASCII' in simd/ascii.go (revive)

simd/memchr_digit.go

[failure] 18-18:
confusing-naming: Method 'MemchrDigit' differs only by capitalization to function 'memchrDigit' in simd/memchr_digit_amd64.go (revive)

simd/memchr_class_amd64.go

[failure] 28-28:
confusing-naming: Method 'memchrNotWord' differs only by capitalization to function 'MemchrNotWord' in simd/memchr_class.go (revive)


[failure] 20-20:
confusing-naming: Method 'memchrWord' differs only by capitalization to function 'MemchrWord' in simd/memchr_class.go (revive)

simd/memchr.go

[failure] 50-50:
confusing-naming: Method 'MemchrPair' differs only by capitalization to function 'memchrPair' in simd/memchr_amd64.go (revive)


[failure] 35-35:
confusing-naming: Method 'Memchr3' differs only by capitalization to function 'memchr3' in simd/memchr_amd64.go (revive)


[failure] 25-25:
confusing-naming: Method 'Memchr2' differs only by capitalization to function 'memchr2' in simd/memchr_amd64.go (revive)

🔇 Additional comments (30)
ascii.go (1)

4-5: LGTM!

Also applies to: 15-32

internal/unsafeconv/unsafeconv.go (1)

16-25: LGTM!

search.go (1)

5-6: LGTM!

Also applies to: 21-27, 72-78

search_test.go (1)

330-346: LGTM!

simd/LICENSE (1)

1-22: LGTM!

simd/simd_test.go (1)

1-572: LGTM!

simd/fuzz_test.go (1)

1-117: LGTM!

simd/bench_test.go (1)

1-217: LGTM!

README.md (3)

132-136: 📐 Maintainability & Code Quality | ⚡ Quick win

"See the simd section below for amd64 numbers" doesn't hold for IndexAny2/IndexAny3.

The amd64 benchmark block added at lines 552-632 (sourced from simd/bench_test.go) only contains Memchr2/Memchr3/MemchrPair/MemchrDigit/MemchrNotWord/Memmem/IsASCII rows — there are no Benchmark_IndexAny2/Benchmark_IndexAny3 entries, since those top-level wrappers aren't benchmarked in simd/bench_test.go. The identical pointer for IsASCII (lines 184-186) is satisfied by that block; this one isn't. Either add amd64 IndexAny2/IndexAny3 benchmark rows (likely from a top-level bench file) or reword the note to point at the underlying Memchr2/Memchr3/MemchrPair numbers it actually dispatches to.


27-451: LGTM!


184-186: LGTM!

Also applies to: 501-633

simd/simd.go (1)

26-38: LGTM!

simd/memchr_other.go (1)

11-21: LGTM!

simd/ascii_amd64.s (1)

33-114: LGTM!

simd/memchr_amd64.go (1)

23-46: LGTM!

simd/memchr_amd64.s (1)

35-412: LGTM!

simd/memchr_class_amd64.s (1)

28-401: LGTM!

simd/memchr_digit_amd64.go (1)

17-22: LGTM!

simd/memchr_digit_amd64.s (1)

32-158: LGTM!

simd/cpu_amd64.go (1)

1-14: LGTM!

simd/cpu_other.go (1)

1-8: LGTM!

simd/ascii.go (1)

1-99: LGTM!

simd/ascii_other.go (1)

1-13: LGTM!

simd/memchr.go (1)

1-170: LGTM!

simd/memchr_class.go (1)

1-137: LGTM!

simd/memchr_class_other.go (1)

1-18: LGTM!

simd/memchr_digit.go (1)

1-66: LGTM!

simd/memchr_digit_other.go (1)

1-13: LGTM!

simd/memmem.go (1)

1-132: LGTM!

simd/byte_frequencies.go (1)

1-96: LGTM!

Comment thread simd/ascii_amd64.go Outdated
claude added 2 commits July 21, 2026 14:36
The simd benchmark block has no IndexAny2/IndexAny3 rows; the amd64
numbers for those functions are the Memchr2/Memchr3 kernel rows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017QqEx79JhskHN7sRz5Pj5R
FirstNonASCII's overlapping-final-word hit and MemchrDigitAt's
in-bounds miss were the only unexecuted statements; package simd is now
at 100% statement coverage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017QqEx79JhskHN7sRz5Pj5R

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Performance Alert ⚠️

Possible performance regression was detected for benchmark.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 1.50.

Benchmark suite Current: 92b3894 Previous: 6488cc3 Ratio
Benchmark_TrimSpace/fiber/empty (github.com/gofiber/utils/v2) 0.6256 ns/op 0 B/op 0 allocs/op 0.3128 ns/op 0 B/op 0 allocs/op 2
Benchmark_TrimSpace/fiber/empty (github.com/gofiber/utils/v2) - ns/op 0.6256 ns/op 0.3128 ns/op 2
Benchmark_IndexAny2/32B/swar (github.com/gofiber/utils/v2) - MB/s 4869.29 MB/s 3236.64 MB/s 1.50
Benchmark_IndexAny2/64B/swar (github.com/gofiber/utils/v2) - MB/s 8897.76 MB/s 4022.3 MB/s 2.21
Benchmark_IndexAny2/512B/swar (github.com/gofiber/utils/v2) - MB/s 32081.86 MB/s 4917.01 MB/s 6.52
Benchmark_IndexAny3/32B/swar (github.com/gofiber/utils/v2) - MB/s 4455.84 MB/s 2776.28 MB/s 1.60
Benchmark_IndexAny3/64B/swar (github.com/gofiber/utils/v2) - MB/s 8199.53 MB/s 3255.26 MB/s 2.52
Benchmark_IndexAny3/512B/swar (github.com/gofiber/utils/v2) - MB/s 29337.53 MB/s 3778.63 MB/s 7.76
Benchmark_IsASCII/512B/swar (github.com/gofiber/utils/v2) - MB/s 37021.15 MB/s 15793.8 MB/s 2.34

This comment was automatically generated by workflow using github-action-benchmark.

gaby commented Jul 21, 2026

Copy link
Copy Markdown
Member Author

Note on the failing benchmark / benchmark check: the alert is inverted, not a real regression.

7 of the 8 flagged rows are MB/s (throughput) entries where the "Current" value is 1.5x-7.8x higher than master — e.g. Benchmark_IndexAny2/512B/swar: 32,061 MB/s vs 4,917 MB/s. github-action-benchmark treats every Go metric as smaller-is-better, so the throughput improvements this PR intentionally delivers register as regressions. The ns/op rows for the same benchmarks (the meaningful direction) all improved and did not alert.

The one genuine ns/op row, Benchmark_TrimSpace/fiber/empty (0.31ns → 0.62ns), is on code this PR does not touch: it is a sub-nanosecond single-branch fast path that is sensitive to code alignment on shared runners, and TrimSpace's other sub-benchmarks did not move.

This alert will re-fire on every run of this PR as long as the speedups exist. A durable fix would live in the shared workflow (gofiber/.github benchmark.yml): either exclude MB/s entries from alerting or mark them bigger-is-better.


Generated by Claude Code

@gaby

gaby commented Jul 21, 2026

Copy link
Copy Markdown
Member Author

Note on the failing benchmark / benchmark check: the alert is inverted, not a real regression.

7 of the 8 flagged rows are MB/s (throughput) entries where the "Current" value is 1.5x-7.8x higher than master — e.g. Benchmark_IndexAny2/512B/swar: 32,061 MB/s vs 4,917 MB/s. github-action-benchmark treats every Go metric as smaller-is-better, so the throughput improvements this PR intentionally delivers register as regressions. The ns/op rows for the same benchmarks (the meaningful direction) all improved and did not alert.

The one genuine ns/op row, Benchmark_TrimSpace/fiber/empty (0.31ns → 0.62ns), is on code this PR does not touch: it is a sub-nanosecond single-branch fast path that is sensitive to code alignment on shared runners, and TrimSpace's other sub-benchmarks did not move.

This alert will re-fire on every run of this PR as long as the speedups exist. A durable fix would live in the shared workflow (gofiber/.github benchmark.yml): either exclude MB/s entries from alerting or mark them bigger-is-better.

Generated by Claude Code

@ReneWerner87 Check this out, it found a bug in the benchmark tool 🤣

@ReneWerner87

Copy link
Copy Markdown
Member

Ok sure

@sixcolors

Copy link
Copy Markdown
Member

About a year ago I prototyped something similar for Fiber utils ToLower (https://github.com/sixcolors/fiber_tolower_test/tree/for-gaby). Even a cheap branch to special-case short strings (HEAD/GET/etc.) regressed vs the simple path, and hand-rolling SIMD across ISAs didn’t feel worth the upkeep.

Glad this lands the other way: one simd package, AVX2 only on amd64 for ≥ MinLen, SWAR elsewhere. That matches what I’d want here.

Two open questions:

  1. Is the top-level IsASCII / IndexAny2 / IndexAny3 prologue still a wash, or even a regression, on the sub-32 header-sized inputs Fiber hits most?

  2. Long-term, is the plan to stay AVX2-only + SWAR, or do you expect more backends (e.g. arm64 NEON, AVX-512)?

gaby commented Jul 21, 2026

Copy link
Copy Markdown
Member Author

Good questions — data for both:

1. Sub-32B cost of the prologue

Structurally, sub-32B inputs pay exactly one extra compare-and-branch on amd64: the gate is n >= simd.MinLen && simd.Accelerated() with the length test first, so short inputs never reach the Accelerated() load. Off amd64 the whole branch (including the length test) is dead-code eliminated, because Accelerated() is a compile-time false there — arm64 binaries are bit-identical to the old code path.

Measured (benchstat -count=10, AVX2 Xeon, cloud runner) on the sub-32B rows:

IndexAny2/7B    7.79ns → 9.00ns   +15%  (+1.2ns)
IndexAny2/8B    3.73ns → 3.66ns    ~    (p=0.075)
IndexAny2/16B   5.42ns → 5.14ns   -5%
IndexAny3/7B    7.97ns → 8.56ns    ~    (p=0.436)
IndexAny3/8B    5.03ns → 4.64ns   -8%
IndexAny3/16B   6.68ns → 6.93ns    ~    (p=0.160)
IsASCII/7B      8.52ns → 8.76ns    ~    (p=0.669)
IsASCII/8B      4.10ns → 4.99ns   +22%  (+0.9ns)
IsASCII/16B     5.37ns → 6.09ns   +13%  (+0.7ns)

So: a wash within noise on most rows, with worst observed swings of ±1ns at the 4-9ns scale — and the sign flips between rows and between runs, which points at code-layout/alignment effects rather than the branch itself (a predicted compare is ~0.3ns on these cores; some rows improved despite the added branch). For the short tokens Fiber hits most (GET, header names), that's the whole story; header values like Accept/Cookie/User-Agent/URLs routinely cross 32B and get the -38% to -88% side of the trade.

2. Backend plan

The dispatch seam is built for more backends without API churn: each function is exported wrapper → per-GOARCH *Impl hook → kernel or shared SWAR fallback, so a NEON backend is one *_arm64.go + .s pair per family, with the exported API, fallbacks, tests (which pin every impl against scalar references), and MinLen untouched. Whether to add NEON is a maintainer call on upkeep-vs-win — the sweep/fuzz harness makes verifying a new kernel cheap, which was most of the historical cost you hit in the ToLower prototype. AVX-512 was deliberately left out: narrower availability, downclocking on older Intel, and 32B/iter already saturates these scan loops at HTTP sizes. Meanwhile arm64 isn't stuck on byte loops — the shared fallbacks are word-wide SWAR built on the swar package.


Generated by Claude Code

@gaby
gaby marked this pull request as ready for review July 22, 2026 02:07
claude added 2 commits July 22, 2026 13:02
- SelectRareBytes: prefer a distinct second rare byte, so needles with a
  duplicated rarest byte (e.g. "zze") stay eligible for the paired
  prefilter instead of collapsing to Byte1 == Byte2; document that
  Index1/Index2 are first occurrences and that distinct values are
  guaranteed whenever the needle has two
- Memmem: hand over to bytes.Index after exactly memmemMaxMisses failed
  verifications (one iteration earlier, matching the documented budget);
  drop the now-redundant Index1 != Index2 gate; describe the
  paired/single routing accurately in the docs
- swar.ZeroLanes: document the every-true-zero-lane guarantee that
  memchrPairGeneric's mask AND relies on, and note the reliance there
- tests: cover memmemPaired's miss-budget handover platform-independently
  (dense false-pair haystacks), pin memmemFallback's index translation,
  assert first-occurrence and distinct-value selection properties, and
  replace the tautological Test_Accelerated assertion with independent
  checks against x/sys/cpu and GOARCH
- fuzz: seed FuzzMemmem so both prefilter paths reach memmemFallback
  (absent and found-by-fallback) from the seed corpus alone
- bench: add Benchmark_Memmem_Prefilter, which forces the prefilter
  helpers at every size with the needle shapes Memmem actually routes to
  them, making the 128-byte routing constant measurable from the file
- CI: set GOFIBER_SIMD_REQUIRE_AVX2 in the Test workflow so amd64
  runners fail loudly if the kernels silently fall back
- docs: refresh stale scalar-tail comments and provenance notes in the
  amd64 kernel files, align the package/README prose with the actual
  Memmem routing, and regenerate the amd64 benchmark block

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017QqEx79JhskHN7sRz5Pj5R
- bench: Benchmark_Memmem_Prefilter's comment described benchData as
  uniform-random with a ~256-byte anchor recurrence; it is a repeating
  'a'..'w' cycle whose 'q' anchor recurs every 23 bytes, so the miss
  budget engages from a few hundred bytes, not kilobyte sizes
- memchrPairGeneric: fix the inverted ordering claim — after ANDing the
  ZeroLanes masks a flagged lane may be a false candidate strictly below
  the first true pair, which is exactly why candidates are re-verified
- package doc: only Memmem's paired path runs on a package kernel
  (MemchrPair); the single-rare-byte path scans with bytes.IndexByte
- memchr_amd64 header and provenance note: describe the pair kernel's
  two-load tail (byte1 window ending at len-offset, byte2 at the buffer
  end) instead of the single-vector phrasing that only fits memchr2/3;
  align the README phrase
- tests: assert Byte2-minimality among values distinct from Byte1 in the
  SelectRareBytes property loop and add an "ezZq" promotion-path case,
  closing the gap where a frozen-Byte2 implementation passed the suite

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017QqEx79JhskHN7sRz5Pj5R
…ackage doc

- tests: add a SelectRareBytes case whose Byte2 value repeats ("zzee"),
  the only shape that distinguishes first- from last-occurrence Index2;
  every existing needle's Byte2 value appeared exactly once, so a
  last-occurrence regression previously passed the whole suite
- package doc: short single-valued needles take the single rare-byte
  scan, not bytes.Index, and the paired path requires two distinct
  values rather than exactly two — mirror Memmem's own doc wording

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017QqEx79JhskHN7sRz5Pj5R
@ReneWerner87
ReneWerner87 merged commit 02367e2 into master Jul 22, 2026
15 of 16 checks passed
@ReneWerner87
ReneWerner87 deleted the claude/simd-utils-integration-b2ls05 branch July 22, 2026 14:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Proposal: SIMD-accelerated variants (AVX2/NEON) of hot byte-scanning helpers

4 participants