DotMatch Performance Improvements - 2026-07-05
This benchmark note covers practical performance improvements in the current codebase: Python callers were not reaching native exact/Hamming kernels, several higher-level Python workflows ignored their Hamming metric, and the threaded native exact-count path mishandled offset windows.
Implemented Improvements
Bound
qdaln_index_assign_hamming_statsin the Pythonctypeslayer.Bound
qdaln_index_lookup_exact_many_statsin the Pythonctypeslayer.Bound
qdaln_index_lookup_exact_ascii_many_statsin the Pythonctypeslayer.Bound
qdaln_index_assign_status_statsin the Pythonctypeslayer.Added top-level
dotmatch.assign_hamming(...).Added top-level
dotmatch.assign_exact(...).Added
Matcher.assign_hamming(...).Added
Matcher.assign_hamming_with_stats(...).Added
Matcher.assign_exact(...).Added
Matcher.assign_exact_with_stats(...).Added
Matcher.assign_status_with_stats(...)for status-only early-stop use cases.Routed
assign_dataframe(..., metric="hamming")through the Hamming kernel.Routed
assign_dataframe(..., metric="exact")through the exact lookup table.Routed
stream_assign(..., metric="hamming")through the Hamming kernel.Routed
stream_assign(..., metric="exact")through the exact lookup table.Routed
dotmatch.tl.assign_features(..., metric="hamming"|"exact")through the native fast paths.Routed
dotmatch.tl.feature_counts(..., metric="hamming"|"exact")through the native fast paths.Switched AssaySpec fixed-window start scoring to the Hamming kernel.
Replaced streamed assignment TSV
DictWriterrows with direct fixed-column writes.Fixed threaded native Hamming
k=0offset-window counting so batch mode evaluates extracted windows, records totals, and falls back to the threaded direct worker for large buffers.Replaced the generic exact-assignment fallback in native offset detection with the exact ASCII lookup path.
Routed
inspect-unmatched --k 0primary fixed-window assignment through exact ASCII lookup.Routed
inspect-unmatched --offset-window ... --k 0shifted-window hints through exact ASCII lookup.Changed
assignment_summary(...)to accumulate local integer counters instead of mutating a dictionary per row.Changed
write_assignments_tsv(...)to keep local summary counters while writing rows.
Benchmarks
Hardware/environment: local macOS build, make dotmatch, PYTHONPATH=python,
local libdotmatch.dylib, synthetic fixed-window DNA workloads.
Python matcher, 100,000 reads, 4,096 targets, length 20
Path |
Seconds |
Reads/sec |
Notes |
|---|---|---|---|
|
0.367143 |
272,374 |
General Levenshtein path |
|
0.200717 |
498,214 |
1.83x faster |
|
0.194195 |
514,946 |
1.89x faster for exact windows |
|
0.207492 |
481,946 |
1.77x faster when lower-bound ambiguity details are acceptable |
Python streaming FASTQ, 200,000 reads, 1,024 targets, length 20
Path |
Seconds |
Reads/sec |
Notes |
|---|---|---|---|
|
1.666834 |
119,988 |
Previous default route |
|
1.323489 |
151,116 |
1.26x faster with identical assignments for this substitution-only workload |
Native threaded exact count offset regression
Workload: 200,000 reads, target window starts at offset 5, target length 20,
Hamming k=0, 1,024 targets. Before this pass, --threads 4 fed whole reads
to exact lookup and reported total_reads=0, assigned_unique=0,
unmatched=200000.
Path |
Count phase seconds |
Total reads |
Assigned exact |
Unmatched |
|---|---|---|---|---|
|
0.214056 |
200000 |
190000 |
10000 |
|
0.206341 |
200000 |
190000 |
10000 |
The threaded path now matches single-thread counts and is slightly faster on the counting phase for this workload.
Additional exact-window and streaming-summary checks
These checks compare the patched tree against a temporary baseline binary built
from the checked-in pre-change src/qda.c. Outputs were compared byte-for-byte
for the native commands.
Workload |
Baseline seconds |
Patched seconds |
Impact |
|---|---|---|---|
|
0.763317 |
0.684115 |
1.12x faster |
|
0.118202 |
0.116536 |
1.01x faster |
|
0.401176 |
0.152804 |
2.63x faster |