Date: 2025-10-30 Status: Research Complete Scope: Dictionary, Automata, and Compositional Data Structures
Beyond the current SIMD implementation for distance functions, the codebase has significant untapped potential for SIMD acceleration across multiple hot paths:
Top Opportunities (by expected impact):
Overall Impact: Could achieve 2-4x overall speedup for typical fuzzy matching workloads.
Location: src/transducer/transition.rs:21-36
Current Implementation (Scalar):
fn characteristic_vector<'a>(
dict_char: u8,
query: &[u8],
window_size: usize,
offset: usize,
buffer: &'a mut [bool; 8],
) -> &'a [bool] {
let len = window_size.min(8);
for (i, item) in buffer.iter_mut().enumerate().take(len) {
let query_idx = offset + i;
*item = query_idx < query.len() && query[query_idx] == dict_char;
}
&buffer[..len]
}
Why It's Hot: Called for every state transition in Levenshtein automata traversal
SIMD Strategy:
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2")]
unsafe fn characteristic_vector_simd(
dict_char: u8,
query: &[u8],
window_size: usize,
offset: usize,
) -> u8 {
// Broadcast dict_char to all 32 lanes
let dict_vec = _mm256_set1_epi8(dict_char as i8);
// Load up to 32 query bytes (process 8 at a time)
let mut query_bytes = [0u8; 32];
let load_len = (query.len() - offset).min(window_size).min(32);
if offset < query.len() {
let available = query.len() - offset;
let copy_len = available.min(load_len);
query_bytes[..copy_len].copy_from_slice(&query[offset..offset + copy_len]);
}
let query_vec = _mm256_loadu_si256(query_bytes.as_ptr() as *const __m256i);
// Compare all bytes at once
let cmp_result = _mm256_cmpeq_epi8(dict_vec, query_vec);
// Extract mask (1 bit per byte comparison result)
let mask = _mm256_movemask_epi8(cmp_result) as u32;
// Apply window size mask
let window_mask = (1u32 << window_size) - 1;
(mask & window_mask) as u8
}
Benefits:
Complexity: Low Implementation Time: 2-3 hours
Location: src/transducer/state.rs:82-99
Current Implementation (Scalar):
pub fn insert(&mut self, position: Position, algorithm: Algorithm) {
// Check if this position is subsumed by existing ones
for existing in &self.positions {
if existing.subsumes(&position, algorithm) {
return; // Already covered
}
}
// Remove positions that this new position subsumes
self.positions.retain(|p| !position.subsumes(p, algorithm));
self.positions.push(position);
}
// Position::subsumes checks: |i - j| <= (f - e)
Why It's Hot: Called for every candidate position during automata traversal Critical for: High max_distance queries (>3) with many positions per state
SIMD Strategy:
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2")]
unsafe fn batch_subsumption_check(
positions: &[Position],
new_position: &Position,
algorithm: Algorithm,
) -> (bool, Vec<bool>) {
let n = positions.len();
let mut subsumed_by_existing = false;
let mut subsumes_existing = vec![false; n];
// Process 8 positions at a time
for chunk_start in (0..n).step_by(8) {
let chunk_size = (n - chunk_start).min(8);
// Load term indices and errors into vectors
let mut term_indices = [0i32; 8];
let mut num_errors = [0i32; 8];
for i in 0..chunk_size {
term_indices[i] = positions[chunk_start + i].term_index as i32;
num_errors[i] = positions[chunk_start + i].num_errors as i32;
}
// Broadcast new position values
let new_term = _mm256_set1_epi32(new_position.term_index as i32);
let new_errors = _mm256_set1_epi32(new_position.num_errors as i32);
// Load existing position vectors
let exist_term = _mm256_loadu_si256(term_indices.as_ptr() as *const __m256i);
let exist_errors = _mm256_loadu_si256(num_errors.as_ptr() as *const __m256i);
// Compute |term_index - new_term|
let diff = _mm256_sub_epi32(exist_term, new_term);
let abs_diff = _mm256_abs_epi32(diff);
// Compute (new_errors - exist_errors)
let error_diff = _mm256_sub_epi32(new_errors, exist_errors);
// Check if |i-j| <= (f-e)
let subsumes_mask = _mm256_cmpgt_epi32(
_mm256_add_epi32(abs_diff, _mm256_set1_epi32(1)),
error_diff
);
// Extract results
let result_mask = !(_mm256_movemask_epi8(subsumes_mask) as u32);
for i in 0..chunk_size {
if (result_mask >> (i * 4)) & 1 != 0 {
subsumes_existing[chunk_start + i] = true;
}
}
}
(subsumed_by_existing, subsumes_existing)
}
Benefits:
Complexity: Medium Implementation Time: 4-6 hours
Location: src/distance/simd.rs:198-208
Current Status: Falls back to scalar
SIMD Strategy: Implement 128-bit SSE4.1 version (4 u32 values in parallel)
_mm_min_epu32, _mm_cmpeq_epi32, _mm_andnot_si128Benefits:
Complexity: Low (copy AVX2 with 128-bit intrinsics) Implementation Time: 2-3 hours
Location: src/distance/mod.rs:302-357
Current Implementation: Triple-loop DP with scalar operations
SIMD Strategy: Same as standard distance
Benefits:
Complexity: Medium Implementation Time: 4-6 hours
Location: src/distance/mod.rs:108-145
Current Implementation: Linear character-by-character comparison
SIMD Strategy:
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2")]
unsafe fn find_first_mismatch_simd(a: &[char], b: &[char]) -> usize {
let len = a.len().min(b.len());
let mut i = 0;
// Process 8 chars at a time
while i + 8 <= len {
let mut a_chars = [0u32; 8];
let mut b_chars = [0u32; 8];
for j in 0..8 {
a_chars[j] = a[i + j] as u32;
b_chars[j] = b[i + j] as u32;
}
let a_vec = _mm256_loadu_si256(a_chars.as_ptr() as *const __m256i);
let b_vec = _mm256_loadu_si256(b_chars.as_ptr() as *const __m256i);
let cmp = _mm256_cmpeq_epi32(a_vec, b_vec);
let mask = _mm256_movemask_epi8(cmp);
// Check if all equal
if mask != -1 {
// Found mismatch - count trailing ones
return i + (mask as u32).trailing_ones() as usize / 4;
}
i += 8;
}
// Handle remainder
while i < len && a[i] == b[i] {
i += 1;
}
i
}
Benefits:
Complexity: Low Implementation Time: 2-3 hours
Location: Multiple dictionary implementations (dawg.rs, suffix_automaton.rs, etc.)
Current Implementation: Linear search for <16 edges
SIMD Strategy:
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2")]
unsafe fn find_edge_simd(edges: &[(u8, usize)], target: u8) -> Option<usize> {
if edges.len() == 0 { return None; }
if edges.len() > 32 { return None; } // Fall back to binary search
// Extract labels into array
let mut labels = [0u8; 32];
for (i, (label, _)) in edges.iter().enumerate() {
labels[i] = *label;
}
// Broadcast target
let target_vec = _mm256_set1_epi8(target as i8);
let labels_vec = _mm256_loadu_si256(labels.as_ptr() as *const __m256i);
// Compare
let cmp = _mm256_cmpeq_epi8(target_vec, labels_vec);
let mask = _mm256_movemask_epi8(cmp) as u32;
if mask != 0 {
let idx = mask.trailing_zeros() as usize;
if idx < edges.len() {
return Some(edges[idx].1);
}
}
None
}
Benefits:
Complexity: Low Implementation Time: 2-3 hours
Location: src/transducer/state.rs:173-186
Current Implementation: Linear scan to find minimum
SIMD Strategy:
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2")]
unsafe fn min_distance_simd(positions: &[Position]) -> usize {
if positions.is_empty() { return usize::MAX; }
if positions.len() == 1 { return positions[0].num_errors; }
// Process 8 at a time
let mut min_vec = _mm256_set1_epi32(i32::MAX);
for chunk in positions.chunks(8) {
let mut errors = [i32::MAX; 8];
for (i, pos) in chunk.iter().enumerate() {
errors[i] = pos.num_errors as i32;
}
let errors_vec = _mm256_loadu_si256(errors.as_ptr() as *const __m256i);
min_vec = _mm256_min_epi32(min_vec, errors_vec);
}
// Horizontal minimum
let min_arr = std::mem::transmute::<__m256i, [i32; 8]>(min_vec);
min_arr.iter().min().unwrap() as usize
}
Benefits:
Complexity: Low Implementation Time: 1-2 hours
Strategy: Use _mm_prefetch to load next node before accessing it
#[cfg(target_arch = "x86_64")]
use std::arch::x86_64::{_mm_prefetch, _MM_HINT_T0};
unsafe fn traverse_with_prefetch(&self, path: &[u8]) {
let mut state = 0;
for (i, &byte) in path.iter().enumerate() {
// Prefetch next potential state
if i + 1 < path.len() {
let predicted_next = self.predict_next_state(state, path[i + 1]);
_mm_prefetch(
&self.nodes[predicted_next] as *const _ as *const i8,
_MM_HINT_T0
);
}
state = self.transition(state, byte)?;
}
}
Benefits:
Use Case: Efficient bit extraction from masks in PathMap
#[cfg(all(target_arch = "x86_64", target_feature = "bmi2"))]
use std::arch::x86_64::{_pext_u64, _pdep_u64};
// Extract set bits efficiently
let set_bits = _pext_u64(mask, 0xFFFFFFFFFFFFFFFF);
let count = set_bits.count_ones();
Strategy: Align hot structures to 64-byte cache lines
#[repr(align(64))] // CPU cache line size
struct CacheOptimizedState {
// Hot fields first
positions: SmallVec<[Position; 8]>,
index: usize,
// Cold fields last
metadata: StateMetadata,
}
Benefits:
Duration: 1-2 weeks Expected Overall Speedup: 2-3x on typical workloads
Tasks:
Week 1:
Week 2:
Duration: 1-2 weeks Expected Additional Speedup: 1.3-1.5x
Tasks:
| Optimization | Impact | Complexity | Time | Priority |
|---|---|---|---|---|
| Characteristic Vector SIMD | Very High | Low | 2-3h | 1 ⭐⭐⭐ |
| Edge Lookup SIMD | High | Low | 2-3h | 2 ⭐⭐⭐ |
| Position Subsumption SIMD | Very High | Medium | 4-6h | 3 ⭐⭐⭐ |
| SSE4.1 Fallback | High | Low | 2-3h | 4 ⭐⭐ |
| Common Prefix/Suffix SIMD | Medium | Low | 2-3h | 5 ⭐⭐ |
| Transposition Distance SIMD | High | Medium | 4-6h | 6 ⭐⭐ |
| Min Distance SIMD | Medium | Low | 1-2h | 7 ⭐ |
| Prefetching | Medium | Medium | 3-4h | 8 ⭐ |
| Cache Line Alignment | Low | Low | 2-3h | 9 ⭐ |
| BMI2 Instructions | Low | Medium | 3-4h | 10 ⭐ |
src/simd/
├── mod.rs // Public API, runtime detection
├── x86_64/
│ ├── mod.rs
│ ├── avx2.rs // AVX2 implementations
│ ├── sse41.rs // SSE4.1 implementations
│ └── scalar.rs // Fallback implementations
├── aarch64/
│ ├── mod.rs
│ └── neon.rs // ARM NEON implementations
└── generic.rs // Cross-platform scalar fallbacks
pub struct SimdCapabilities {
pub has_avx2: bool,
pub has_sse41: bool,
pub has_bmi2: bool,
pub has_popcnt: bool,
}
static SIMD_CAPS: LazyLock<SimdCapabilities> = LazyLock::new(|| {
SimdCapabilities::detect()
});
Create comprehensive benchmarks for each optimization:
// benches/simd_benchmarks.rs
criterion_group!(
name = simd_benches;
config = Criterion::default();
targets =
bench_characteristic_vector,
bench_position_subsumption,
bench_edge_lookup,
bench_transposition_distance,
// ...
);
Compare:
| Workload Type | Baseline | Phase 4 | Total Gain |
|---|---|---|---|
| Low max_distance (≤2) | 100% | 180-220% | 1.8-2.2x |
| Medium max_distance (3-4) | 100% | 250-300% | 2.5-3x |
| High max_distance (≥5) | 100% | 300-400% | 3-4x |
| Workload Type | Baseline | Phase 4+5 | Total Gain |
|---|---|---|---|
| Low max_distance | 100% | 220-280% | 2.2-2.8x |
| Medium max_distance | 100% | 320-400% | 3.2-4x |
| High max_distance | 100% | 400-500% | 4-5x |
The codebase has tremendous potential for SIMD acceleration beyond distance functions:
✅ High-impact targets identified: Characteristic vectors, subsumption, edge lookups ✅ Low-hanging fruit: Many optimizations are low complexity, high impact ✅ Proven approach: Can use same patterns as Phase 3 distance SIMD ✅ Incremental deployment: Can implement and validate each optimization independently
Recommendation: Implement Phase 4 high-impact optimizations for 2-4x overall speedup on typical fuzzy matching workloads.
Analysis Date: 2025-10-30 Status: Ready for Implementation Estimated Total Effort: 1-2 weeks for Phase 4
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |