Skip to content

Perf/optimize leading trailing#17

Merged
jcfangc merged 75 commits into
mainfrom
perf/optimize-leading-trailing
Jul 4, 2026
Merged

Perf/optimize leading trailing#17
jcfangc merged 75 commits into
mainfrom
perf/optimize-leading-trailing

Conversation

@jcfangc

@jcfangc jcfangc commented Jul 4, 2026

Copy link
Copy Markdown
Owner

No description provided.

jcfangc and others added 30 commits June 28, 2026 02:45
…ne benchmarks

Const-generify the fill-selector parameter from runtime u64 to
compile-time const FILL_ONES: bool across the full call chain:

  BitStr layer
  ├── impls_for_leading_zeros.rs   → leading_value_count<FILL_ONES>
  └── impls_for_trailing_zeros.rs  → trailing_value_count<FILL_ONES>
       (extracted from the former — the two scan directions now live
       in separate modules)

  SIMD dispatch layer (traits/bits_arith)
  ├── funcs_for_leading_value_words.rs  → leading dispatch only
  └── funcs_for_trailing_value_words.rs → trailing dispatch only
       (each contains its own scalar/avx2/sse41/neon backend, all
       parameterised by const FILL_ONES)

  Scalar helpers
  ├── count_trailing<FILL_ONES>, count_leading<FILL_ONES>,
  │   count_leading_within<FILL_ONES> — native u64 bit-count
  │   intrinsics selected at compile time
  └── scalar::scan<FILL_ONES> / scan_rev<FILL_ONES>

The runtime if fill == 0 branches that selected between
leading_zero_words() / leading_one_words() and between
trailing_zero_words() / trailing_one_words() are now dead-code
eliminated in each monomorphisation.

Benchmarks (30 scenarios, bit_ops_leading_trailing):
  leading_zeros:  ~8-9% faster on small inputs, ~2-8% on large
  trailing_zeros: ~8% faster across the board

Build hygiene:
- Remove .cargo/config.toml (global target-cpu=native migrated to
  CI benchmark step env only)
- pre-commit: cargo test now runs with RUSTFLAGS=-C target-cpu=native
  so SIMD backend equivalence tests are exercised on every commit

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: DeepSeek AI <service@deepseek.com>
Replace const FILL_ONES: bool with const FILL: u64 in
leading/trailing value-word scans.  The fill value (0 / u64::MAX) is
now passed directly as a const generic, eliminating the runtime
if-branch in every backend.

Introduce funcs_for_value_words_core following the binary_core
pattern — FILL_ZEROS / FILL_ONES constants live in the core module,
and callers import them with clear semantics.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: DeepSeek AI <service@deepseek.com>
Hot path: one vptest (FILL=0) or vpxor+vptest (FILL=!0) per SIMD chunk.
On mismatch the chunk is delegated to the scalar loop — no lane-level
movemask scanning, no unreachable_unchecked, no per-element cmpeq.

FILL_ZEROS / FILL_ONES are now pub(crate) in consts_for_bits.
BitsArith gains leading_value_words<FILL> / trailing_value_words<FILL>
as const-generic trait methods, superseding the four named methods.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: DeepSeek AI <service@deepseek.com>
…t scans

Removes the leading_value_words / trailing_value_words trait methods and
the entire funcs_for_value_words_core module.  Instead, leading_zeros
and trailing_zeros now call chunk_eq (a single-purpose SIMD helper that
checks whether a chunk equals FILL via vptest) directly from the
bit-level count functions.

This flattens the call chain from:
  BitStr::leading_zeros → leading_value_count → trait method →
    SMALL_WORDS dispatch → SIMD backend → word-count × 64 → TZCNT
to:
  BitStr::leading_zeros → leading_value_count → chunk_eq SIMD loop →
    TZCNT

The SIMD hot path is now 1-2 instructions (vptest / vpxor+vptest) per
chunk with no lane-scanning, no dispatch, and no word→bit translation.

Bench: leading_zeros len_4096 all_zeros 51.8→33.1 ns (-36%),
len_65 all_zeros 19.9→13.0 ns (-35%), dense cases now faster than
bitvec_simd.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: DeepSeek AI <service@deepseek.com>
…methods

Move chunk_eq from bit_str/impls_for_bit_arith/ into the new
funcs_for_value_bits_core module under traits/bits_arith.

Add leading_value_bits and trailing_value_bits (with WORD_ALIGNED
const-generic) to the BitsArith trait, implemented for [u64].

The new core functions leading() and trailing() are not yet wired
into the callers — this commit establishes the infrastructure.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: DeepSeek AI <service@deepseek.com>
Replace leading_value_count and trailing_value_count free functions
with pub(crate) inner methods on BitStr:
  - leading_value_bits_inner::<FILL, WORD_ALIGNED>(&self)
  - trailing_value_bits_inner::<FILL, WORD_ALIGNED>(&self)

Both trim the word slice and delegate to the BitsArith trait methods.
Public APIs check self.start % WORD_BITS at runtime and route to the
appropriate monomorphised variant (WORD_ALIGNED=true for aligned views).

The old free functions and their helpers (count_trailing, count_leading,
count_leading_within) are removed from these files — they live in
funcs_for_value_bits_core now.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: DeepSeek AI <service@deepseek.com>
Covers aligned and unaligned paths for both FILL_ZEROS and FILL_ONES
across a representative set of input slices.  Each test compares the
SIMD-accelerated implementation against a pure-scalar oracle.

Also fix a debug-mode usize overflow in trailing() by using
wrapping_sub for the chunk_start computation.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: DeepSeek AI <service@deepseek.com>
Replace funcs_for_leading_trailing_core module (with sub-module
chunk_eq) with three flat files at bits_arith level:

  funcs_for_chunk_eq.rs     — shared SIMD primitive (vptest)
  funcs_for_leading_core.rs — leading::<FILL, WORD_ALIGNED>
  funcs_for_trailing_core.rs — trailing::<FILL, WORD_ALIGNED>

This matches the pattern of funcs_for_eq_words_{aligned,unaligned}_core
and eliminates unnecessary module nesting.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: DeepSeek AI <service@deepseek.com>
Add bits_equal_at_inner::<HS_WORD_ALIGNED, ND_WORD_ALIGNED> as the
const-generic core, and route starts_with through matches_at_inner.
When both alignment signals are true, the nd_base % WORD_BITS runtime
check is eliminated by LLVM.

This establishes the two-sequence alignment pattern:
  - Single sequence: WORD_ALIGNED (leading/trailing)
  - Two sequences: HS_WORD_ALIGNED + ND_WORD_ALIGNED (matches/starts_with)

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: DeepSeek AI <service@deepseek.com>
…signals

Add pub(crate) inner methods on BitStr:
  - starts_with_inner::<HS, ND>(prefix)
  - ends_with_inner::<HS, ND>(suffix, offset)

BitString calls these directly with HS_WORD_ALIGNED=true, eliminating
the haystack-side runtime alignment check.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: DeepSeek AI <service@deepseek.com>
When HS_WORD_ALIGNED is true (BitString path), the start%WORD_BITS==0
unaligned branch is eliminated at compile time, going straight to the
aligned SIMD find_any_candidate path.

BitString::contains() calls contains_inner::<true, {nd_aligned}>(needle).

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: DeepSeek AI <service@deepseek.com>
All matching APIs now use suffixes to indicate argument type:

  _str    — argument is BitStr (alignment unknown, runtime check)
  _string — argument is &BitString (word-aligned, compile-time signal)

New methods added:
  BitStr::starts_with_string(&BitString)  — prefix aligned
  BitStr::ends_with_string(&BitString)    — suffix aligned
  BitStr::contains_string(&BitString)     — needle aligned
  BitString::starts_with_string(&BitString) — both aligned, zero checks
  BitString::ends_with_string(&BitString)   — both aligned, zero checks
  BitString::contains_string(&BitString)    — both aligned, zero checks

Renamed:
  starts_with → starts_with_str
  ends_with → ends_with_str
  contains → contains_str
  find → find_str
  rfind → rfind_str
  matches_at → matches_at_str

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: DeepSeek AI <service@deepseek.com>
Add pub(crate) count_ones_inner with WORD_ALIGNED const-generic.
When true, the unaligned path (first/last partial word handling)
is eliminated at compile time.

BitStr::count_ones() routes via 2-way dispatch on start%WORD_BITS.
BitString::count_ones() already calls [u64]::count_ones directly
(always aligned), so no change needed.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: DeepSeek AI <service@deepseek.com>
Rename cmp → cmp_str, add cmp_string(&BitString), and extract
cmp_inner with compile-time alignment signals.

BitStr:
  cmp_str(BitStr)    — 4-way match on both alignments
  cmp_string(&BS)    — 2-way (other is word-aligned)
  cmp_inner::<HS, ND> — const-generic core

BitString:
  cmp_str(BitStr)    — 2-way (self is word-aligned)
  cmp_string(&BS)    — zero checks (both aligned)
  Ord::cmp delegates to cmp_string

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: DeepSeek AI <service@deepseek.com>
Add pub(crate) inner methods for find and rfind with WORD_ALIGNED
const-generic.  When true, the unaligned path is eliminated.

BitStr:
  find_str/find_string/find_inner::<WORD_ALIGNED>
  rfind_str/rfind_string/rfind_inner::<WORD_ALIGNED>

BitString:
  find_str/rfind_str call inner::<true> directly
  find_string/rfind_string added (both sides aligned)

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: DeepSeek AI <service@deepseek.com>
matches_at_str now does 4-way runtime alignment dispatch on both
(self.start + index) and pattern.start.  Added matches_at_string
for &BitString patterns.  BitString calls inner directly with
HS_WORD_ALIGNED=true.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: DeepSeek AI <service@deepseek.com>
Extract hash_inner from Hash::hash impl.  BitString::hash calls
hash_inner::<true, H> directly, skipping the runtime alignment branch.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: DeepSeek AI <service@deepseek.com>
Now does 4-way match on both self.start and other.start before
calling bits_equal_at_inner, instead of always passing false,false.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: DeepSeek AI <service@deepseek.com>
Rename strip_prefix → strip_prefix_str, strip_suffix → strip_suffix_str.
Add _string variants and inner methods with HS/ND_WORD_ALIGNED.
BitString delegates to BitStr::starts_with_string/ends_with_string for
optimal alignment routing.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: DeepSeek AI <service@deepseek.com>
find_inner and rfind_inner now take ND_WORD_ALIGNED in addition to
WORD_ALIGNED, and pass both through bits_equal_at_inner instead of
the lossy bits_equal_at (which hardcoded false,false).

find_str and rfind_str now do 4-way alignment dispatch (matching
contains_str pattern).  BitString strip methods call _inner directly
with HS_WORD_ALIGNED=true, skipping the redundant runtime check.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: DeepSeek AI <service@deepseek.com>
bits_equal_at always passed ::<false, false>, silently dropping
alignment info.  Delete it — all callers must now explicitly choose
their alignment path via bits_equal_at_inner.

Callers in test files use ::<false, false> (correct default for
general-purpose tests).

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: DeepSeek AI <service@deepseek.com>
HS_WORD_ALIGNED represents (self.start + offset) % WORD_BITS == 0,
not just self.start % WORD_BITS == 0.  For starts_with offset=0 so
both are equivalent, but ends_with has offset > 0.

Also rename eq_words params for clarity:
  other→needle, count→full_words, offset→haystack_shift.
The trait impl no longer does word-slicing — the caller (inner)
pre-trims words and computes the shift.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: DeepSeek AI <service@deepseek.com>
When the caller passes HS_WORD_ALIGNED=true, hs_base % WORD_BITS is
known to be 0 at compile time.  Branch on the const-generic to avoid
the runtime modulo and pass 0 directly.

This closes the cycle: the const-generic now eliminates BOTH the
runtime branch in eq_words AND the modulo computation in the caller.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: DeepSeek AI <service@deepseek.com>
Single entry point eq_words::<HS_WORD_ALIGNED> dispatches to the
aligned or unaligned path.  Backends (avx2/sse41/neon) live in one
file.  The trait impl delegates directly without a branch.

Delete funcs_for_eq_words_aligned_core.rs and
funcs_for_eq_words_unaligned_core.rs.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: DeepSeek AI <service@deepseek.com>
Same pattern as eq_words: the trait method takes pre-trimmed haystack
+ needle, haystack_shift, and HS_WORD_ALIGNED const-generic.  The impl
dispatches to aligned/unaligned core files.

Inner (cmp_inner) trims words and skips haystack_shift modulo when
HS_WORD_ALIGNED is true.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: DeepSeek AI <service@deepseek.com>
BitsEdit::read_word_at and write_word_at now accept WORD_ALIGNED const
generic.  When true, the cross-word stitch/spill is eliminated.

All callers pass ::<false> (preserving runtime behavior).
contains/find/rfind callbacks pass ::<false> for haystack position.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: DeepSeek AI <service@deepseek.com>
Suppress dead_code warning on strip_prefix/suffix inner methods.
Add ours_str and ours_string entries to all matching bench files
(starts_with, ends_with, find, rfind, contains).  Native String
baselines preserved as "string" entries.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: DeepSeek AI <service@deepseek.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: DeepSeek AI <service@deepseek.com>
All divan bench entries now use ours_str or ours_string suffixes.
Codspeed filter simplified to ours_ to match all our entries uniformly.
starts_with bench rewritten with clean 3-way comparison per group.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: DeepSeek AI <service@deepseek.com>
Move const-generic inner method to impls_for_count_ones/inner.rs.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: DeepSeek AI <service@deepseek.com>
jcfangc and others added 24 commits July 1, 2026 12:57
Move funcs_for_{chunk_eq,leading_core,trailing_core}.rs into
funcs_for_ends/ as chunk_eq.rs, leading.rs, trailing.rs, so
chunk_eq (shared by leading+trailing) is scoped under the same
parent and invisible to count_ones.

Also fix two genuine dead-code warnings:
- ALIGN_THRESHOLD gated behind avx2 cfg (only used by AVX2 path)
- Remove unused chunk_eq_aligned/chunk_eq_2x_aligned (superseded
  by inline raw intrinsics in the AVX2 paths)
- Gate chunk_eq imports and AVX2 extras behind #[cfg] to suppress
  cfg-dependent dead-code warnings in both default and AVX2 builds

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: DeepSeek AI <service@deepseek.com>
Document pointer arithmetic invariants (base, end, p) and the
preconditions ensuring each SIMD backend's raw pointer accesses
stay within bounds.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: DeepSeek AI <service@deepseek.com>
Document chunk_start bounds: wi_end + 1 - (done + STRIDE) is always
≥ 0 because done + STRIDE ≤ total_words ≤ wi_end + 1, so ptr.add(...)
stays within the valid slice range.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: DeepSeek AI <service@deepseek.com>
Document every unsafe raw-pointer dereference in leading_zeros,
leading_ones, trailing_zeros, and trailing_ones with reference to
the BitString invariant: words always contains at least
(bit_len + 63) / 64 elements.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: DeepSeek AI <service@deepseek.com>
Add compile-time-dispatch feature (off by default). In default mode,
CPUID-based AVX2 detection selects the optimal SIMD backend at runtime
on x86/x86_64. The AVX2 scan is extracted into a #[target_feature]
function in the avx2 module.

When compile-time-dispatch is enabled, the current cfg-gated inline
behaviour is preserved.

The scalar remainder scan is shared between both dispatch paths,
executed regardless of which SIMD backend handled the chunk scan.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: DeepSeek AI <service@deepseek.com>
Extract AVX2 backend from trailing.rs into a #[target_feature] function
in the avx2 module, and factor out the runtime CPU detection into a
shared funcs_for_ends/runtime module used by both leading and trailing.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: DeepSeek AI <service@deepseek.com>
Avoid the trait call-chain overhead for dense/alternating patterns by
checking the first/rightmost word inline before delegating to WordsScan.
Use raw pointer access instead of slice indexing to eliminate bounds
checks in the hot path.

len_65/dense: 7.39 ns → 3.22 ns (-56%)
len_65/alternating: 7.71 ns → 3.37 ns (-56%)
BitStr/BitString ratio: 3.9x → 1.7x

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: DeepSeek AI <service@deepseek.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: DeepSeek AI <service@deepseek.com>
Same pattern as binary_core: two top-level #[cfg] blocks — default
mode uses CPUID leaf-7 runtime AVX2 detection, compile-time-dispatch
mode preserves the existing #[cfg] cascade.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: DeepSeek AI <service@deepseek.com>
Add runtime CPUID detection for all SIMD levels (AVX2, SSE4.1,
SSSE3, SSE2) in every backend dispatch function, replacing the
AVX2-only runtime detection with full multi-level detection.

- SSE4.1 files (7): cmp_aligned, cmp_unaligned, eq_words_aligned,
  eq_words_unaligned, find_first_word, find_any_candidate, find_last_word
- SSSE3 files (1): count_ones (with len >= N guards preserved)
- SSE2 files (3): pack_bools, pack_str, copy_words_shifted
- Restructured chunk_eq.rs: mod imp + re-export → named modules +
  dispatch fn with AtomicU8-cached CPUID
- Updated words_arith files (4): binary, not, shl, shr now also
  runtime-detect SSE2 via CPUID leaf 1, EDX bit 26

Default mode: all x86 SIMD levels runtime-detected via CPUID.
compile-time-dispatch feature: pure #[cfg(target_feature)] cascade
unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: DeepSeek AI <service@deepseek.com>
…shifts

Remove count_leading_within helper which caused redundant shifts:
- Last partial word: single  replaces
  mask+shift or shift+shift patterns
- First partial word: use  directly since leading_zeros
  naturally ignores low bits, eliminating the self-canceling
   double shift

Follows the hand-inlined pattern from leading.rs for consistency.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: DeepSeek AI <service@deepseek.com>
… BitStr

Covers exhaustive length 0..256, single-bit at every position,
cross-word boundaries (including SIMD thresholds), last-word mask
invariant, and systematic BitStr oracle across misaligned views.

9 attack tests, ~40K oracle comparisons.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: DeepSeek AI <service@deepseek.com>
Categories covered:
- from_raw_parts in BitStr inner dispatch (2 files)
- __cpuid_count in runtime feature detection (15 files)
- Runtime SIMD backend dispatch calls (15 files)
- Compile-time SIMD backend dispatch calls (15 files)
- SIMD intrinsic operations in backend modules (5 files)

19 files, +315 SAFETY comments, matching existing conventions.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: DeepSeek AI <service@deepseek.com>
The file-level ALIGN_THRESHOLD (cfg: target_feature=avx2) was a
duplicate of the module-level constant inside mod avx2.  When
compile-time-dispatch+AVX2 is active both exist, so the inline AVX2
block can reference avx2::ALIGN_THRESHOLD directly instead.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: DeepSeek AI <service@deepseek.com>
Delete ~90 lines of duplicated inline AVX2 intrinsics; both runtime and
compile-time dispatch now call avx2::leading_scan.  Remove simd_done flag
and restructure dispatch into two clean blocks matching the reference
pattern (funcs_for_eq_words_unaligned_core.rs).

Extract SSE2 and NEON backends into named modules (mod sse2, mod neon)
each with a #[target_feature] leading_scan function.

Benchmarks verified no regression:
- leading_zeros/len_4096/all_zeros: 20.03 ns (baseline 20.48 ns)
- leading_zeros/len_65536/all_zeros: 172 ns (baseline 172.1 ns)
- leading_zeros/len_65/dense: 2.88 ns (baseline 3.28 ns)

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: DeepSeek AI <service@deepseek.com>
Mirror the leading.rs refactoring: delete ~60 lines of duplicated inline
AVX2 intrinsics, remove simd_done flag, restructure dispatch into two
clean blocks (runtime CPUID + compile-time #[cfg] cascade).

Extract SSE2 and NEON backends into named modules (mod sse2, mod neon),
each with a #[target_feature] trailing_scan function returning usize
(new 'done' count). Both dispatch blocks uniformly compute the scanned
delta from the returned done value.

Benchmarks verified no regression (default mode, AVX2):
- trailing_zeros/len_4096/all_zeros: 25.7 ns (baseline 24.8 ns, ~noise)
- trailing_zeros/len_65536/all_zeros: 237.7 ns (baseline 228.8 ns, ~4%)
- trailing_zeros/len_65/dense: 5.9 ns (baseline 8.7 ns, improved)

All 138 tests pass both default and compile-time-dispatch modes.
aarch64 cross-check passes.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: DeepSeek AI <service@deepseek.com>
Merge chunk_eq.rs + runtime.rs + leading.rs into funcs_for_leading_core.rs
and trailing.rs into funcs_for_trailing_core.rs, following the reference
pattern from funcs_for_eq_words_unaligned_core.rs et al.

- Delete chunk_eq.rs: SSE2/NEON backends now use private #[inline(always)]
  chunk_eq helpers with raw SIMD intrinsics, eliminating the separate
  runtime dispatch (double-dispatch) on every chunk.
- Delete runtime.rs: CPUID cache extracted to shared mod cpuid in mod.rs.
- Delete funcs_for_ends.rs: replaced by mod.rs.
- AVX2 leading_scan: fix inverted misalign formula (was (base%32)/8, now
  (4 - (base/8)%4) % 4 — words to next 32-byte boundary).
- AVX2 trailing_scan: inline raw intrinsics (no chunk_eq dependency).
- All backend functions: #[target_feature] only, no #[inline] (matching
  reference pattern).
- Dispatch functions: #[inline] (matching reference pattern).

Benchmark results (x86_64, vs pre-refactoring baseline):
  len_65/all_zeros/ours_str:   12.5ns (-22%)   | 8.1ns native (-21%)
  len_65/all_zeros/ours_string: 10.7ns (-25%)   | 3.4ns native (-17%)
  len_4096/all_zeros/ours_str:  19.7ns (-8%)    | 17.9ns native (-21%)
  len_4096/all_zeros/ours_string: 17.7ns (-12%) | 16.0ns native (-28%)
  len_65536/all_zeros/ours_str: 161ns (tied)    | 178ns native (-3%)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: DeepSeek AI <service@deepseek.com>
Replace 16 scattered CPUID call sites (15 uncached + 1 cached) with a
single OnceCell-style CpuFeatures struct at crate root.

- src/cpuid.rs: AtomicU8 state machine detects AVX2, SSE4.1, SSSE3, SSE2
  exactly once on first call; subsequent calls read a single Relaxed load.
- Details: https://github.com/jcfangc/bit-string/issues/none

All 15 inline CPUID blocks replaced with
followed by  /  /  / .

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: DeepSeek AI <service@deepseek.com>
The crate is single-threaded; there is no need for an atomic state machine
with compare_exchange spin-wait.  A simple null-pointer check + Box::leak
is sufficient and removes the last AtomicU8 from the crate.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: DeepSeek AI <service@deepseek.com>
Replace the hand-rolled raw-pointer OnceCell with once_cell::sync::OnceCell.
Adds once_cell = "1" dependency (already widely used in the ecosystem,
zero-cost on stable).

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: DeepSeek AI <service@deepseek.com>
Change #[inline(always)] to #[inline] on [u64]::leading_value_bits.
When compiled without target-cpu=native, leading() was small enough that
LLVM inlined it through the trait call, pulling SIMD dispatch dead code
into BitStr::leading_value_bits_inner for scalar inputs.  The weaker hint
lets LLVM's heuristics decide, which keeps the scalar path lean.

Narrows len_65/all_zeros ours_str gap (dynamic vs static dispatch):
  12.41ns -> 12.42ns (neutral) without native
  9.67ns -> 10.70ns (slightly slower) with native
  gap: 28% -> 16%

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: DeepSeek AI <service@deepseek.com>
When the compile-time target lacks BMI1 (no target-cpu=native) but the
CPU supports it, dispatch the tiny-input scalar loop to a
#[target_feature(enable = "bmi1")] copy that uses tzcnt instead of bsf.
Gated with cfg!(target_feature = "bmi1") to avoid redundant dispatch
when BMI1 is already the baseline.  Also add bmi1 to CpuFeatures.

x86-only, gated with #[cfg(any(target_arch = "x86", target_arch =
"x86_64"))] for aarch64 compatibility.

len_65/all_zeros:
  ours_str:  12.42 -> 11.92ns without native, 10.70 -> 10.73ns with native
  ours_str gap: 28% -> 11%
  ours_string: 10.56 -> 10.21ns without native, 4.25 -> 4.28ns with native

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: DeepSeek AI <service@deepseek.com>
.cargo/config.toml is gitignored to avoid breaking CI (gate scalar test
requires baseline x86-64, bench-pages already sets target-cpu=native).
Copy the example file to activate locally.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: DeepSeek AI <service@deepseek.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: DeepSeek AI <service@deepseek.com>
@codspeed-hq

codspeed-hq Bot commented Jul 4, 2026

Copy link
Copy Markdown

Merging this PR will create unknown performance changes

🆕 216 new benchmarks
⏩ 182 skipped benchmarks1

Performance Changes

Benchmark BASE HEAD Efficiency
🆕 count_ones/len_4096/alternating/ours_string N/A 772.2 ns N/A
🆕 count_ones/len_4096/dense/ours_string N/A 772.2 ns N/A
🆕 count_ones/len_4096/sparse/ours_string N/A 772.2 ns N/A
🆕 count_ones/len_65/alternating/ours_string N/A 368.1 ns N/A
🆕 count_ones/len_65/dense/ours_string N/A 368.1 ns N/A
🆕 count_ones/len_65/sparse/ours_string N/A 368.1 ns N/A
🆕 count_ones/len_65536/alternating/ours_string N/A 5.1 µs N/A
🆕 count_ones/len_65536/dense/ours_string N/A 5 µs N/A
🆕 count_ones/len_65536/sparse/ours_string N/A 5 µs N/A
🆕 leading_zeros/len_4096/all_zeros/ours_str N/A 836.4 ns N/A
🆕 leading_zeros/len_4096/all_zeros/ours_string N/A 744.7 ns N/A
🆕 leading_zeros/len_4096/dense/ours_str N/A 211.9 ns N/A
🆕 leading_zeros/len_4096/dense/ours_string N/A 150 ns N/A
🆕 leading_zeros/len_65/all_zeros/ours_str N/A 645.8 ns N/A
🆕 leading_zeros/len_65/all_zeros/ours_string N/A 553.6 ns N/A
🆕 leading_zeros/len_65/alternating/ours_str N/A 211.9 ns N/A
🆕 leading_zeros/len_65/alternating/ours_string N/A 150 ns N/A
🆕 leading_zeros/len_65/dense/ours_str N/A 211.9 ns N/A
🆕 leading_zeros/len_65/dense/ours_string N/A 150 ns N/A
🆕 leading_zeros/len_65536/all_zeros/ours_str N/A 4.7 µs N/A
... ... ... ... ...

ℹ️ Only the first 20 benchmarks are displayed. Go to the app to view all benchmarks.


Comparing perf/optimize-leading-trailing (578a926) with main (770e07b)

Open in CodSpeed

Footnotes

  1. 182 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

jcfangc and others added 2 commits July 4, 2026 08:51
…ews, ones, invariants

Six new attack test sections (J through O) covering previously untested
scenarios discovered during the perf/optimize-leading-trailing refactor:

J. Exhaustive leading_ones/trailing_ones — all lengths 0..256 with single
   zero at each position (mirrors Section A for zeros).
K. Very long strings (256..65536 bits) — exercises SIMD accumulation and
   the ALIGN_THRESHOLD boundary (128 words = 8192 bits).
L. BitStr deep unaligned views — starts beyond the first word (offsets up
   to 1023) with unaligned offsets, verified against round-trip oracle.
M. Leading/trailing ones cross-word boundaries — mirrors Section C for
   all-ones background with zero transitions.
N. Randomized cross-operation invariants — LCG-driven pseudo-random
   patterns verifying bounds, mutual exclusion, and BitStr oracle.
O. Stress interleaving mutation (set/push/pop/truncate) with
   leading/trailing counts — verifies last-word mask after edits.

All 144 adversarial tests pass.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: DeepSeek AI <service@deepseek.com>
Replace the single generic example with:
- Quick start section with copy-paste examples
- Per-category sections: construction, BitStr views, querying, editing,
  matching/searching, bitwise ops, comparison/hashing/iteration
- Each section has a runnable code block demonstrating the API
- Keep SIMD backend table and benchmark links

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: DeepSeek AI <service@deepseek.com>
@jcfangc
jcfangc merged commit fe4564c into main Jul 4, 2026
7 checks passed
@jcfangc
jcfangc deleted the perf/optimize-leading-trailing branch July 4, 2026 09:12
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.

2 participants