Target: successors_i_type() method in src/transducer/generalized/state.rs
Current Performance: 2.75% of total cycles (largest single function hotspot)
Expected Improvement: 2-4% additional speedup after H2
Date: 2025-11-18
The successors_i_type() method is the largest single function hotspot identified in baseline profiling at 2.75% of total cycles. After completing H2 optimization (character vector caching, 28-31% speedup), this method remains the primary target for further optimization.
Key Bottlenecks Identified:
.to_string() calls for can_apply() checks (lines 297-298, 316, 332, 349-350, 417-418, 473)From PHASE1_FLAMEGRAPH_ANALYSIS.md:
Post-H2 Status:
can_apply() callsProblem: Multiple .to_string() calls create temporary heap allocations for can_apply() checks.
Current Code Pattern:
// Line 297-298: Match operation
let word_char_str = word_slice_chars[match_index].to_string();
let input_char_str = input_char.to_string();
if op.can_apply(word_char_str.as_bytes(), input_char_str.as_bytes()) {
// ...
}
// Line 316: Delete operation
let word_char_str = word_slice_chars[match_index].to_string();
if op.can_apply(word_char_str.as_bytes(), &[]) {
// ...
}
// Line 332: Insert operation
let input_char_str = input_char.to_string();
if op.can_apply(&[], input_char_str.as_bytes()) {
// ...
}
// Line 349-350: Substitute operation
let word_char_str = word_slice_chars[match_index].to_string();
let input_char_str = input_char.to_string();
if op.can_apply(word_char_str.as_bytes(), input_char_str.as_bytes()) {
// ...
}
// Line 417-418: Merge operation (2,1)
let word_2chars: String = word_slice_chars[match_index..match_index+2].iter().collect();
let input_1char = input_char.to_string();
if op.can_apply(word_2chars.as_bytes(), input_1char.as_bytes()) {
// ...
}
// Line 473: Split operation (1,2)
let word_1char = word_slice_chars[match_index].to_string();
if op.can_apply_to_source(word_1char.as_bytes()) {
// ...
}
Analysis:
.to_string() allocates on the heap&[u8] via .as_bytes()Proposed Solution 1a: Pre-compute UTF-8 Byte Slices
Pre-compute UTF-8 byte representations once per method call:
// At method beginning (after line 275):
let word_slice_bytes: Vec<&[u8]> = word_slice_chars.iter()
.map(|c| {
// Create a small stack buffer for each character's UTF-8 encoding
// Most chars are 1-4 bytes, stored inline without heap allocation
let mut buf = [0u8; 4];
let s = c.encode_utf8(&mut buf);
s.as_bytes() // This borrows from buf, need different approach
})
.collect();
Issue: Lifetime problem - need to store the encoded bytes somewhere persistent.
Proposed Solution 1b: Use char::encode_utf8() with Stack Buffer
// For single character operations:
let mut word_buf = [0u8; 4];
let word_bytes = word_slice_chars[match_index].encode_utf8(&mut word_buf).as_bytes();
let mut input_buf = [0u8; 4];
let input_bytes = input_char.encode_utf8(&mut input_buf).as_bytes();
if op.can_apply(word_bytes, input_bytes) {
// ...
}
Benefits:
Proposed Solution 1c: Pre-encode Input Character Once
The input character is constant throughout the method:
// At method beginning (after line 273):
let mut input_char_buf = [0u8; 4];
let input_char_bytes = input_char.encode_utf8(&mut input_char_buf).as_bytes();
Then reuse input_char_bytes everywhere instead of input_char.to_string().as_bytes().
Expected Impact:
Problem: Operations are filtered multiple times with intermediate Vec allocations.
Current Code:
// Lines 285-361: Loop over ALL operations for standard ops
for op in operations.operations() {
if op.consume_x() > 1 || op.consume_y() > 1 {
continue; // Skip multi-char ops
}
// Handle match, delete, insert, substitute
}
// Lines 365-367: Filter for transpose operations
let transpose_ops: Vec<_> = operations.operations().iter()
.filter(|op| op.consume_x() == 2 && op.consume_y() == 2)
.collect();
// Lines 421-445: Loop over ALL operations for merge
for op in operations.operations() {
if op.consume_x() == 2 && op.consume_y() == 1 {
// Handle merge
}
}
// Lines 452-454: Filter for split operations
let split_ops: Vec<_> = operations.operations().iter()
.filter(|op| op.consume_x() == 1 && op.consume_y() == 2)
.collect();
Analysis:
Proposed Solution 2a: Pre-categorize Operations at Automaton Construction
Modify OperationSet to store operations by category:
// In src/transducer/operation_set.rs:
pub struct OperationSet {
all_operations: Vec<OperationType>,
// Pre-categorized for fast access:
standard_ops: Vec<OperationType>, // (1,1) operations
transpose_ops: Vec<OperationType>, // (2,2) operations
merge_ops: Vec<OperationType>, // (2,1) operations
split_ops: Vec<OperationType>, // (1,2) operations
}
Then in successors_i_type:
// No filtering needed!
for op in operations.standard_ops() {
// Handle match, delete, insert, substitute
}
if !operations.transpose_ops().is_empty() && errors < self.max_distance {
// Handle transpose (no Vec allocation)
}
for op in operations.merge_ops() {
// Handle merge
}
if !operations.split_ops().is_empty() && can_enter_split {
// Handle split (no Vec allocation)
}
Benefits:
Effort: Medium (requires OperationSet refactoring)
Expected Impact: 0.2-0.5% overall speedup (eliminate filtering overhead)
Proposed Solution 2b: Use Iterator Chains (Lower Effort Alternative)
If OperationSet refactoring is too invasive, use iterator chains without collecting:
// Lines 365-367: Don't collect, just check existence
let has_transpose = operations.operations().iter()
.any(|op| op.consume_x() == 2 && op.consume_y() == 2);
if has_transpose && errors < self.max_distance {
for op in operations.operations().iter()
.filter(|op| op.consume_x() == 2 && op.consume_y() == 2)
{
// Handle transpose
}
}
Benefits: No Vec allocation, lower refactoring effort
Tradeoff: Still multiple iterations, less optimal than 2a
Expected Impact: 0.1-0.3% overall speedup
Problem: successors Vec allocates on heap even for small result sets.
Current Code:
fn successors_i_type(...) -> Vec<GeneralizedPosition> {
let mut successors = Vec::new();
// ...
successors
}
Analysis:
Proposed Solution:
use smallvec::{SmallVec, smallvec};
fn successors_i_type(...) -> SmallVec<[GeneralizedPosition; 8]> {
let mut successors = SmallVec::new();
// ... rest unchanged
successors
}
Benefits:
Effort: Low (type change + update call sites)
Expected Impact: 0.2-0.4% overall speedup (reduce allocation overhead)
Note: SmallVec already imported in the codebase, so dependency exists.
Problem: Multiple sequential passes over operations could be merged.
Current Structure:
Proposed Solution:
Single loop with operation categorization:
for op in operations.operations() {
match (op.consume_x(), op.consume_y()) {
(1, 1) if op.is_match() => {
// Handle match
}
(1, 1) if op.is_substitution() => {
// Handle substitute
}
(1, 0) => {
// Handle delete
}
(0, 1) => {
// Handle insert
}
(2, 2) => {
// Handle transpose
}
(2, 1) => {
// Handle merge
}
(1, 2) => {
// Handle split
}
_ => {}
}
}
Tradeoff Analysis:
Risk: Might not improve performance due to branch prediction overhead
Recommendation: DEFER - Try Strategies 1-3 first, then profile again
Steps:
input_char once at method beginninginput_char.to_string() with input_char_bytesword_char.to_string() with encode_utf8() stack bufferExpected Time: 2-3 hours Expected Improvement: 0.5-1.0% speedup Risk: Low (straightforward refactoring)
Steps:
Option A (High effort, high reward): Refactor OperationSet
Option B (Low effort, medium reward): Iterator chains
collect() with iterator chainsExpected Time:
Expected Improvement:
Risk:
Recommendation: Start with Option B, upgrade to Option A if profiling shows filtering overhead is significant
Steps:
SmallVec<[GeneralizedPosition; 8]>successors_i_type()successors_m_type()successors_i_transposing()successors_m_transposing()successors_i_splitting()successors_m_splitting()Expected Time: 2-3 hours Expected Improvement: 0.2-0.4% speedup Risk: Low (type change only)
Conservative Estimate: 0.8-1.5% overall speedup Optimistic Estimate: 1.2-1.9% overall speedup
Combined with H2:
OperationSet Refactoring: Is pre-categorization worth the effort?
SmallVec Capacity: What's the optimal inline capacity?
Post-H2 Hotspots: Are there new bottlenecks after H2?
can_apply() Internals: Can we optimize the operation matching logic?
OperationType::can_apply after Strategy 1 implementeddocs/optimization/PHASE1_FLAMEGRAPH_ANALYSIS.mddocs/optimization/H2_RESULTS.md (28-31% speedup achieved)src/transducer/generalized/state.rs:260-569 (successors_i_type)src/transducer/operation_type.rssrc/transducer/operation_set.rsCan 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 |