Identified significant Arc overhead in query traversal (query.rs:103). The current implementation clones DictionaryNode (Arc) for every explored edge to preserve the parent chain, but the node is never used in parents - only the label is needed for path reconstruction.
Optimization Opportunity: Replace full Intersection cloning with lightweight path representation → 15-30% potential improvement
pub struct Intersection<N: DictionaryNode> {
pub label: Option<u8>, // Edge label from parent
pub node: N, // Current dictionary node (Arc for DAWG!)
pub state: State, // Automaton state
pub parent: Option<Box<Intersection<N>>>, // Parent chain
}
Issue: The entire Intersection is stored in the parent chain, including the node field.
fn queue_children(&mut self, intersection: &Box<Intersection<N>>) {
for (label, child_node) in intersection.node.edges() {
if let Some(next_state) = transition_state_pooled(/*...*/) {
// ❌ PROBLEM: Clone entire intersection to preserve parent chain
let parent_box = Box::new(Intersection {
label: intersection.label,
node: intersection.node.clone(), // ← Arc::clone for DAWG!
state: intersection.state.clone(),
parent: intersection.parent.clone(),
});
let child = Box::new(Intersection::with_parent(
label,
child_node,
next_state,
parent_box, // ← Stores full node in parent chain
));
self.pending.push_back(child);
}
}
}
Arc Overhead:
intersection.node.clone() performs Arc::clonepub fn term(&self) -> String {
let mut bytes = Vec::new();
self.collect_path(&mut bytes); // ← Recursively walks parent chain
bytes.reverse();
String::from_utf8_lossy(&bytes).into_owned()
}
fn collect_path(&self, bytes: &mut Vec<u8>) {
if let Some(label) = self.label {
bytes.push(label); // ← Only uses label!
if let Some(parent) = &self.parent {
parent.collect_path(bytes); // ← Recursively collects labels
}
}
}
Key Insight: Path reconstruction only accesses label and parent fields - the node field in parents is never used!
From flame graph analysis (before optimizations):
After Arc-free contains() optimization:
Estimated remaining Arc overhead in queries: 15-20% of query execution time
Replace full Intersection in parent chain with lightweight path-only structure:
/// Lightweight parent representation (no node, just path)
struct PathNode {
label: u8,
parent: Option<Box<PathNode>>,
}
pub struct Intersection<N: DictionaryNode> {
pub label: Option<u8>,
pub node: N,
pub state: State,
pub parent: Option<Box<PathNode>>, // ← Lightweight!
}
Benefits:
Complexity: Medium - requires modifying Intersection structure and path methods
Expected improvement: 15-25%
Create specialized query iterator for DAWG that works with node indices:
struct DawgQueryIterator {
// Work with indices instead of DawgDictionaryNode
pending: VecDeque<DawgIntersection>,
dawg_nodes: Arc<Vec<DawgNode>>, // Shared reference, no per-node Arc
// ...
}
struct DawgIntersection {
label: Option<u8>,
node_idx: usize, // ← Index instead of DawgDictionaryNode
state: State,
parent: Option<Box<PathNode>>,
}
Benefits:
contains())Complexity: High - requires specialized implementation per dictionary type
Trade-offs: Less generic, more maintenance burden
Use Rc instead of Box for parent chain sharing:
pub parent: Option<Rc<PathNode>>,
Benefits:
Complexity: Low
Expected improvement: 5-10% (helps with cloning but doesn't eliminate Arc from node)
Implement Strategy 1: Lightweight Parent Chain
Rationale:
Implementation Plan:
PathNode struct with label and parent onlyIntersection to use Option<Box<PathNode>> for parentqueue_children to create PathNode instead of full Intersectioncollect_path to work with PathNode/// Lightweight representation of path history.
///
/// Used to reconstruct the term path without storing full Intersection data.
/// This eliminates Arc overhead from dictionary node cloning in parent chains.
pub struct PathNode {
/// Edge label from parent
pub label: u8,
/// Parent in the path
pub parent: Option<Box<PathNode>>,
}
impl PathNode {
/// Create a new path node
pub fn new(label: u8, parent: Option<Box<PathNode>>) -> Self {
Self { label, parent }
}
/// Collect labels into vector (for term reconstruction)
pub fn collect_labels(&self, labels: &mut Vec<u8>) {
labels.push(self.label);
if let Some(parent) = &self.parent {
parent.collect_labels(labels);
}
}
}
pub struct Intersection<N: DictionaryNode> {
pub label: Option<u8>,
pub node: N,
pub state: State,
pub parent: Option<Box<PathNode>>, // ← Changed from Intersection to PathNode
}
impl<N: DictionaryNode> Intersection<N> {
pub fn with_parent(
label: u8,
node: N,
state: State,
parent: Option<Box<PathNode>>, // ← Updated signature
) -> Self {
Self {
label: Some(label),
node,
state,
parent,
}
}
pub fn term(&self) -> String {
let mut bytes = Vec::new();
// Collect current label
if let Some(label) = self.label {
bytes.push(label);
}
// Collect parent labels
if let Some(parent) = &self.parent {
parent.collect_labels(&mut bytes);
}
bytes.reverse();
String::from_utf8_lossy(&bytes).into_owned()
}
}
fn queue_children(&mut self, intersection: &Box<Intersection<N>>) {
for (label, child_node) in intersection.node.edges() {
if let Some(next_state) = transition_state_pooled(/*...*/) {
// ✅ Create lightweight path node (no Arc clone!)
let parent_path = if let Some(current_label) = intersection.label {
Some(Box::new(PathNode::new(
current_label,
intersection.parent.clone(), // Clone PathNode chain (cheap)
)))
} else {
None
};
let child = Box::new(Intersection::with_parent(
label,
child_node,
next_state,
parent_path, // ← Lightweight path, no node clone!
));
self.pending.push_back(child);
}
}
}
| Operation | Baseline | +Arc contains | +Arc query | Total Improvement |
|---|---|---|---|---|
| Contains 1M | 203ms | 81ms | - | 60% (2.5x) |
| Query 5k | 4.71s | 3.89s | ~3.2s | 32% (1.47x) |
Overall: Query operations would be 1.47x faster, reaching ~32% total improvement over baseline.
For maximum performance, implement DAWG-specific query iterator (Strategy 2):
Potential: 25-35% improvement (full Arc elimination) Cost: Higher maintenance, less generic code
Recommend starting with Strategy 1 (lightweight parent chain) and evaluating if further optimization is needed.
Intersection::with_parent signature changes (parent type)Intersection::parent field type changesMitigation: This is likely internal API, but check for external usage.
Estimated development time: 2-4 hours Expected impact: 17-21% query performance improvement Risk level: Medium (API changes, but isolated to query internals)
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 |