Skip to content

[go 1.27rc2] perf: BM25 SIMD/scalar accumulator dispatch with struct-aware binary search#8

Open
shoriwe wants to merge 10 commits into
mainfrom
perf/bm25-simd
Open

[go 1.27rc2] perf: BM25 SIMD/scalar accumulator dispatch with struct-aware binary search#8
shoriwe wants to merge 10 commits into
mainfrom
perf/bm25-simd

Conversation

@shoriwe

@shoriwe shoriwe commented Jul 23, 2026

Copy link
Copy Markdown
Member

Summary

This PR refactors the BM25 hot path into two separate accumulator implementations, scalar and AVX2, selected at runtime based on CPU capability. It also eliminates all slices.BinarySearchFunc calls in the scoring and storage layers, replacing them with inlined, struct-aware binary search methods that avoid function call overhead and closure allocations.

Changes

  • Extracted accumulateBM25 into ScalarAccumulateBM25 (query/bm5_acc_scalar.go) and AVX2AccumulateBM25 (query/bm25_acc_avx2.go). The dispatch wrapper AccumulateBM25 selects the AVX2 path when archsimd.X86.AVX2() is true and ForceScalar is not set; falls back to scalar otherwise.
  • Removed the binarysearch package dependency from storage/storage.go. All binary search calls across Tokens, TokenFrequencies, DocumentsLengths, and Scoring now use methods defined directly on their respective types (BinarySearchString, BinarySearchBytes, BinarySearch). This eliminates pointer indirection and closure overhead on every call in the hot path.
  • Added TokenFrequencies and DocumentsLengths named slice types in storage/headers.go to host the new methods. Updated Field.DocumentLengths and Storage.TokenFrequencies field types accordingly.
  • Deleted binarysearch/pointer.go as it is no longer used.
  • Added ForceScalar env var (FORCE_SCALAR=1) to force the scalar path for benchmarking and debugging.
  • Added ForceScalar bool field to Searcher for programmatic override.
  • NormalizedTF and ScoreTermBM25 are no longer public API; tests that exercised them independently have been removed. Correctness is now enforced at the end-to-end level (TestPropertyInvariantsExtended), where the forward/reverse scoring delta tolerance was relaxed from 1e-12 to 1e-5 to accommodate SIMD rounding.
  • Updated go.mod to go 1.27rc2.

Performance context (i7-1280P, 46M hits, n=1)

Metric Baseline (scalar 4x) Scalar 8x AVX2 64-float Scalar 8x vs baseline AVX2 vs baseline
WithCopy latency 2.989s ± 2% 1.153s ± 7% 1.480s ± 2% 2.59x faster (61.4%) 1.98x faster (50.5%)
WithNoCopy latency 2.979s ± 2% 1.099s ± 1% 1.480s ± 1% 2.71x faster (63.1%) 1.97x faster (50.3%)
geomean latency 2.984s 1.126s 1.480s 2.65x faster (62.3%) 1.98x faster (50.4%)
WithCopy throughput 58.71 MiB/s ± 1% 152.16 MiB/s ± 7% 118.56 MiB/s ± 2% 2.59x higher (+159.2%) 2.02x higher (+101.9%)
WithNoCopy throughput 58.91 MiB/s ± 2% 159.65 MiB/s ± 1% 118.58 MiB/s ± 1% 2.71x higher (+171.0%) 2.01x higher (+101.3%)
geomean throughput 58.81 MiB/s 155.9 MiB/s 118.6 MiB/s 2.65x higher (+165.0%) 2.02x higher (+101.6%)
WithCopy memory 714.5 MiB ± 0% 714.5 MiB ± 0% 714.5 MiB ± 0% unchanged unchanged
WithNoCopy memory 714.5 MiB ± 0% 714.5 MiB ± 0% 714.5 MiB ± 0% unchanged unchanged
WithCopy allocs/op 20.27k ± 0% 20.27k ± 0% 20.27k ± 0% unchanged unchanged
WithNoCopy allocs/op 20.27k ± 0% 20.27k ± 0% 20.27k ± 0% unchanged unchanged

The struct-aware binary search accounts for most of the gain. The AVX2 path is currently slower than scalar 8x on this workload, which is expected given gather overhead dominates at this hit count. The AVX2 path is kept for future tuning and for hardware where the balance shifts.

Raw output

goos: linux
goarch: amd64
pkg: github.com/RogueTeam/textiplex/query
cpu: 12th Gen Intel(R) Core(TM) i7-1280P
                               │ prev-scalar.txt │          after-scalar.txt          │        after-vectorized.txt        │
                               │     sec/op      │   sec/op    vs base                │   sec/op    vs base                │
CustomIndexSearchWithCopy-20          2.989 ± 2%   1.153 ± 7%  -61.41% (p=0.000 n=10)   1.480 ± 2%  -50.48% (p=0.000 n=10)
CustomIndexSearchWithNoCopy-20        2.979 ± 2%   1.099 ± 1%  -63.10% (p=0.000 n=10)   1.480 ± 1%  -50.32% (p=0.000 n=10)
geomean                               2.984        1.126       -62.27%                  1.480       -50.40%

                               │ prev-scalar.txt │            after-scalar.txt            │          after-vectorized.txt          │
                               │       B/s       │      B/s       vs base                 │      B/s       vs base                 │
CustomIndexSearchWithCopy-20        58.71Mi ± 1%   152.16Mi ± 7%  +159.18% (p=0.000 n=10)   118.56Mi ± 2%  +101.94% (p=0.000 n=10)
CustomIndexSearchWithNoCopy-20      58.91Mi ± 2%   159.65Mi ± 1%  +171.01% (p=0.000 n=10)   118.58Mi ± 1%  +101.30% (p=0.000 n=10)
geomean                             58.81Mi         155.9Mi       +165.03%                   118.6Mi       +101.62%

                               │ prev-scalar.txt │          after-scalar.txt           │         after-vectorized.txt          │
                               │      B/op       │     B/op      vs base               │     B/op      vs base                 │
CustomIndexSearchWithCopy-20        714.5Mi ± 0%   714.5Mi ± 0%       ~ (p=1.000 n=10)   714.5Mi ± 0%       ~ (p=1.000 n=10) ¹
CustomIndexSearchWithNoCopy-20      714.5Mi ± 0%   714.5Mi ± 0%       ~ (p=1.000 n=10)   714.5Mi ± 0%       ~ (p=1.000 n=10)
geomean                             714.5Mi        714.5Mi       +0.00%                  714.5Mi       +0.00%
¹ all samples are equal

                               │ prev-scalar.txt │          after-scalar.txt          │         after-vectorized.txt         │
                               │    allocs/op    │  allocs/op   vs base               │  allocs/op   vs base                 │
CustomIndexSearchWithCopy-20         20.27k ± 0%   20.27k ± 0%       ~ (p=1.000 n=10)   20.27k ± 0%       ~ (p=1.000 n=10) ¹
CustomIndexSearchWithNoCopy-20       20.27k ± 0%   20.27k ± 0%       ~ (p=1.000 n=10)   20.27k ± 0%       ~ (p=1.000 n=10)
geomean                              20.27k        20.27k       +0.00%                  20.27k       +0.00%
¹ all samples are equal

@shoriwe shoriwe self-assigned this Jul 23, 2026
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