Recommendation: YES - Highly Recommended
Optimizing the existing DAWG backend is a better investment than adding a double-array trie because:
pub struct DawgDictionary {
nodes: Arc<Vec<DawgNode>>, // Nodes are heap-allocated in Vec
term_count: usize,
}
pub struct DawgNode {
edges: Vec<(u8, usize)>, // Each node has its own heap-allocated Vec
is_final: bool,
}
Problem 1: Poor Cache Locality
DawgNode has its own Vec<(u8, usize)> for edgesProblem 2: Not Memory-Mappable
Vec uses heap pointers that aren't valid across processesmmap() for zero-copy loadingQuery: "test"
Step 1: nodes[0] → edges at memory address 0x1000 → cache miss #1
Step 2: Follow edge 't' → nodes[5] → cache miss #2
edges at memory address 0x2500 → cache miss #3
Step 3: Follow edge 'e' → nodes[12] → cache miss #4
edges at memory address 0x4200 → cache miss #5
...
Result: ~2 cache misses per character in query term
Goal: Store all edges in contiguous memory for better cache performance
Implementation:
/// Cache-friendly DAWG with arena-allocated edges
pub struct OptimizedDawgDictionary {
/// All nodes stored contiguously
nodes: Arc<Vec<OptimizedDawgNode>>,
/// All edges stored in a single contiguous arena
/// Indexed by (offset, length) from OptimizedDawgNode
edge_arena: Arc<Vec<(u8, u32)>>, // (label, target_node_id)
term_count: usize,
}
pub struct OptimizedDawgNode {
/// Offset into edge_arena where this node's edges start
edge_offset: u32,
/// Number of edges (typically 1-5, so u8 is sufficient)
edge_count: u8,
/// True if this node marks the end of a valid term
is_final: bool,
// Total size: 4 + 1 + 1 = 6 bytes (+ 2 bytes padding = 8 bytes)
}
impl OptimizedDawgNode {
fn edges<'a>(&self, arena: &'a [(u8, u32)]) -> &'a [(u8, u32)] {
let start = self.edge_offset as usize;
let end = start + self.edge_count as usize;
&arena[start..end]
}
}
Benefits:
Performance Impact (projected):
Goal: Enable zero-copy loading for instant startup with large dictionaries
Implementation:
use memmap2::Mmap;
/// Memory-mapped DAWG dictionary (zero-copy loading)
pub struct MmapDawgDictionary {
/// Memory-mapped file containing the DAWG data
mmap: Mmap,
/// Header containing metadata
header: DawgHeader,
/// View into nodes section of mmap
nodes: &'static [OptimizedDawgNode],
/// View into edge arena section of mmap
edge_arena: &'static [(u8, u32)],
}
#[repr(C)]
struct DawgHeader {
magic: [u8; 4], // "DAWG" magic number
version: u32, // Format version
node_count: u32, // Number of nodes
edge_count: u32, // Total edges in arena
term_count: u32, // Number of terms
_padding: [u8; 12], // Reserved for future use
}
impl MmapDawgDictionary {
/// Load a DAWG dictionary from a memory-mapped file.
///
/// This provides instant loading without deserialization.
/// The operating system loads pages on-demand as they're accessed.
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
let file = File::open(path)?;
let mmap = unsafe { Mmap::map(&file)? };
// Parse header
let header = Self::parse_header(&mmap)?;
// Create views into mmap sections
let nodes_offset = size_of::<DawgHeader>();
let nodes_size = header.node_count as usize * size_of::<OptimizedDawgNode>();
let nodes = Self::parse_nodes(&mmap, nodes_offset, nodes_size)?;
let edges_offset = nodes_offset + nodes_size;
let edges_size = header.edge_count as usize * size_of::<(u8, u32)>();
let edge_arena = Self::parse_edges(&mmap, edges_offset, edges_size)?;
Ok(Self {
mmap,
header,
nodes,
edge_arena,
})
}
/// Save a DAWG dictionary to a memory-mappable file format.
pub fn to_file<P: AsRef<Path>>(dict: &OptimizedDawgDictionary, path: P) -> Result<(), Error> {
let mut file = File::create(path)?;
// Write header
let header = DawgHeader {
magic: *b"DAWG",
version: 1,
node_count: dict.nodes.len() as u32,
edge_count: dict.edge_arena.len() as u32,
term_count: dict.term_count as u32,
_padding: [0; 12],
};
file.write_all(unsafe {
std::slice::from_raw_parts(
&header as *const _ as *const u8,
size_of::<DawgHeader>()
)
})?;
// Write nodes (already properly aligned)
file.write_all(unsafe {
std::slice::from_raw_parts(
dict.nodes.as_ptr() as *const u8,
dict.nodes.len() * size_of::<OptimizedDawgNode>()
)
})?;
// Write edge arena
file.write_all(unsafe {
std::slice::from_raw_parts(
dict.edge_arena.as_ptr() as *const u8,
dict.edge_arena.len() * size_of::<(u8, u32)>()
)
})?;
Ok(())
}
}
Benefits:
Performance Impact:
Goal: Faster edge lookup for nodes with many edges
Current: Linear scan through edges vector
fn find_edge(&self, label: u8) -> Option<usize> {
self.edges.iter()
.find(|(l, _)| *l == label)
.map(|(_, target)| *target)
}
// O(k) where k = number of edges
Optimized: Binary search for nodes with many edges
fn find_edge(&self, label: u8, arena: &[(u8, u32)]) -> Option<u32> {
let edges = self.edges(arena);
// For small edge counts, linear search is faster than binary search
if self.edge_count <= 4 {
edges.iter()
.find(|(l, _)| *l == label)
.map(|(_, target)| *target)
} else {
// Binary search for nodes with many edges
edges.binary_search_by_key(&label, |(l, _)| *l)
.ok()
.map(|idx| edges[idx].1)
}
}
// O(log k) for large k, O(k) for small k
Benefits:
Performance Impact:
| Aspect | Effort | Risk | Benefit |
|---|---|---|---|
| Implementation | ~1000 lines | Medium | 20-30% faster queries |
| Testing | New test suite | Medium | New code paths |
| Maintenance | Ongoing | Medium | +1 backend to maintain |
| Breaking changes | None | Low | Additive only |
| User migration | Optional | Low | New opt-in feature |
| Total Effort | 5-7 days | - | - |
| Aspect | Effort | Risk | Benefit |
|---|---|---|---|
| Arena allocation | ~200 lines | Low | 15-25% faster + 30% smaller |
| Memory-mapping | ~300 lines | Low | Instant loading |
| Binary search | ~50 lines | Very Low | 10-20% for branching nodes |
| Testing | Extend existing | Low | Same Dictionary trait |
| Maintenance | Minimal | Low | Same backend, better impl |
| Breaking changes | None | Very Low | Internal optimization |
| User migration | Automatic | Very Low | All users benefit |
| Total Effort | 2-3 days | - | - |
Current DAWG (estimated from PathMap benchmarks):
Optimized DAWG (projected):
Reasoning: Better cache locality reduces cache misses by ~20-30%
Current (bincode deserialization for 100k words):
Memory-Mapped (projected):
Reasoning: mmap is O(1) operation, pages loaded on-demand
src/dictionary/dawg_optimized.rs: ~550 lines
- OptimizedDawgDictionary struct: ~50 lines
- Builder with arena allocation: ~200 lines
- Dictionary trait impl: ~100 lines
- Memory-mapped version: ~150 lines
- Tests: ~50 lines
src/dictionary/dawg.rs modifications: ~50 lines
- Add conversion to optimized version
- Deprecation notices
Total new code: ~600 lines (vs ~1000 for DAT)
Breaking changes: None
// New optimized version
pub struct OptimizedDawg { ... }
// Keep old version for compatibility
#[deprecated(since = "0.4.0", note = "Use OptimizedDawg for better performance")]
pub struct DawgDictionary { ... }
impl From<DawgDictionary> for OptimizedDawg {
fn from(old: DawgDictionary) -> Self {
// Convert to optimized format
}
}
impl OptimizedDawg {
pub fn save_to_file(&self, path: &Path) -> Result<()> { ... }
pub fn from_file(path: &Path) -> Result<Self> { ... }
}
impl DictionaryFactory {
pub fn create(...) -> DictionaryContainer {
match backend {
DictionaryBackend::Dawg => {
// Use optimized version by default
DictionaryContainer::Dawg(OptimizedDawg::from_terms(terms))
}
...
}
}
}
Total: 3 days for full implementation
Current DAWG:
Optimized DAWG:
DAT (for comparison):
Winner: Optimized DAWG (95% of DAT benefit, 40% less code)
Current DAWG:
Optimized DAWG:
DAT (for comparison):
Winner: Optimized DAWG (good enough, simpler)
Current DAWG:
Optimized DAWG (mmap):
DAT:
Winner: Optimized DAWG (minimal difference at scale)
Immediate (Week 1):
Short-term (Week 2-3):
Future (only if needed):
OptimizedDawgNode structOptimizedDawgDictionary structDictionary traitmemmap2 dependencyto_file() serializationfrom_file() with mmapDictionaryFactoryTotal: 3 days vs 7 days for DAT
Optimizing the existing DAWG is significantly better than adding DAT because:
The optimized DAWG would provide:
This achieves the main goals of DAT (cache locality + mmap) while:
Recommendation: Implement DAWG optimizations first. Only consider DAT if profiling shows optimized DAWG is still insufficient for specific use cases.
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 |