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

  1. Bound qdaln_index_assign_hamming_stats in the Python ctypes layer.

  2. Bound qdaln_index_lookup_exact_many_stats in the Python ctypes layer.

  3. Bound qdaln_index_lookup_exact_ascii_many_stats in the Python ctypes layer.

  4. Bound qdaln_index_assign_status_stats in the Python ctypes layer.

  5. Added top-level dotmatch.assign_hamming(...).

  6. Added top-level dotmatch.assign_exact(...).

  7. Added Matcher.assign_hamming(...).

  8. Added Matcher.assign_hamming_with_stats(...).

  9. Added Matcher.assign_exact(...).

  10. Added Matcher.assign_exact_with_stats(...).

  11. Added Matcher.assign_status_with_stats(...) for status-only early-stop use cases.

  12. Routed assign_dataframe(..., metric="hamming") through the Hamming kernel.

  13. Routed assign_dataframe(..., metric="exact") through the exact lookup table.

  14. Routed stream_assign(..., metric="hamming") through the Hamming kernel.

  15. Routed stream_assign(..., metric="exact") through the exact lookup table.

  16. Routed dotmatch.tl.assign_features(..., metric="hamming"|"exact") through the native fast paths.

  17. Routed dotmatch.tl.feature_counts(..., metric="hamming"|"exact") through the native fast paths.

  18. Switched AssaySpec fixed-window start scoring to the Hamming kernel.

  19. Replaced streamed assignment TSV DictWriter rows with direct fixed-column writes.

  20. Fixed threaded native Hamming k=0 offset-window counting so batch mode evaluates extracted windows, records totals, and falls back to the threaded direct worker for large buffers.

  21. Replaced the generic exact-assignment fallback in native offset detection with the exact ASCII lookup path.

  22. Routed inspect-unmatched --k 0 primary fixed-window assignment through exact ASCII lookup.

  23. Routed inspect-unmatched --offset-window ... --k 0 shifted-window hints through exact ASCII lookup.

  24. Changed assignment_summary(...) to accumulate local integer counters instead of mutating a dictionary per row.

  25. 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

Matcher.assign_with_stats(..., k=1)

0.367143

272,374

General Levenshtein path

Matcher.assign_hamming_with_stats(..., k=1)

0.200717

498,214

1.83x faster

Matcher.assign_exact_with_stats(...)

0.194195

514,946

1.89x faster for exact windows

Matcher.assign_status_with_stats(..., k=1)

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

stream_assign(..., metric="levenshtein", k=1)

1.666834

119,988

Previous default route

stream_assign(..., metric="hamming", k=1)

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

--threads 1

0.214056

200000

190000

10000

--threads 4

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

count --metric levenshtein --k 0 --auto-offset 8, 300,000 reads, 4,096 targets

0.763317

0.684115

1.12x faster

inspect-unmatched --k 0, 300,000 reads, 4,096 targets

0.118202

0.116536

1.01x faster

assignment_summary(...), 1,000,000 streamed rows

0.401176

0.152804

2.63x faster