Benchmarks

Every performance number ACOR publishes is on this page, with the command that produces it. Nothing here is an estimate.

The evidence comes in two kinds, and they are not interchangeable:

  • Round trips are structural. They are counted at the storage seam, so they are identical on miniredis and on a real server, and no hardware difference can move them. They are enforced by tests on every CI run.
  • Timings are hardware-bound. Absolute nanoseconds on your machine will not match ours. The reproducible quantity is the ratio between configurations.

Round trips per operation

Enforced by TestRTT* in pkg/acor/rtt_claims_test.go. If any of these change, CI fails.

OperationV1V2
Find()11
Find() with EnableCache, warmn/a0
Find() with a Preset enginen/a0
FindParallel() / FindIndexParallel(), 63 chunks11
FindMany(), 3 texts11
Add(), 5-character keyword532
Add(), 26-character keyword5072

Both schemas read in a single round trip: V1 issues one SMEMBERS, V2 pipelines two HGETALL calls into one trip. V1’s round-trip cost is on writes, where it walks the trie node by node, so the cost grows with the length of the keyword being added rather than with the size of the dictionary.

The multi-scan reads cost the same as one Find(), and the count does not grow with the chunk count or the batch size: the automaton is loaded once per call and every chunk or text is scanned against that one snapshot. Before v1.5.0 each chunk loaded its own, so a 63-chunk text issued 63 reads.

go test -run RTT ./pkg/acor

# The same counts against a real server, which is what makes them structural
ACOR_INTEGRATION_ADDR=localhost:6379 go test -run RTT ./pkg/acor

Timings

Measured on: Apple M4, darwin/arm64, Go 1.26, Redis 8 on loopback, -benchtime=200x. The exact command that produced the tables below, which takes about 20 seconds:

redis-server --port 6379 --save "" --daemonize yes
ACOR_INTEGRATION_ADDR=localhost:6379 \
  go test -bench RealServer -benchmem -benchtime=200x -run '^$' ./pkg/acor

make bench runs the full sweep including the miniredis benchmarks. That takes several minutes, and its timings are not published; see the caveats below.

Find, 1000 keywords

Configurationns/opB/opallocs/opvs V1
V1129,06239,5191,070baseline
V2, no cache224,738121,1422,060~1.7x slower
V2 + EnableCache, warm8,6317,89862~15x faster
PresetBalanced2,2042,0484~59x faster

Find, 100 keywords

Configurationns/opB/opallocs/opvs V1
V191,7248,136158baseline
V2, no cache79,88511,144219~1.1x faster
V2 + EnableCache, warm3,1202,85710~29x faster
PresetBalanced2,2142,0484~41x faster

At 100 keywords V1 and V2 are close enough that the winner changes between runs; only the 1000-keyword gap is stable.

Add

Configurationns/opvs V1
V14,956,892baseline
V2353,149~14x faster

Bulk load, AddMany

AddMany plans the whole batch in one pass and commits it in a single transaction, so it costs two round trips regardless of batch size.

On the Apple M4 with Redis 8 on loopback setup above, one sample measured:

Keywordsns/opB/opallocs/op
100422,665199,8501,079
1,0002,958,0261,668,5688,816

Reproduce with ACOR_INTEGRATION_ADDR=localhost:6379 make bench-module.

Run-to-run variance

The ns/op columns are one run. Repeat runs on the same idle laptop moved every absolute number by 20-25% while the ratios held to within about 15%.

That is why the ratios are stated approximately and the raw numbers are labelled a sample. Absolute figures differing from ours is expected; ratios differing substantially is worth reporting as an issue.

What these numbers mean

V2 without caching is still somewhat slower than V1 on reads, because of payload rather than round trips. Both cost one trip, but V1’s SMEMBERS returns just the keyword set while V2 must read an outputs hash carrying one entry per trie state. The gap widens with the dictionary: roughly parity at 100 keywords, ~1.7x at 1000.

Both schemas memoize the automaton, so an unchanged collection is not re-parsed or rebuilt between reads. Uncached V2 was ~9x slower than V1 before that memoization landed; what remains is inherent to reading the whole outputs hash, and EnableCache is the fix for it.

The large read speedups come from caching, not from the schema. 15x to 59x belongs to EnableCache and the Preset engines. Choosing V2 and nothing else does not deliver them.

V2’s unambiguous win is writes. ~14x on Add(), and it is the only schema that supports caching or preset engines at all. Use AddMany rather than a loop over Add: it commits in a single transaction. In the Apple M4/Redis 8 loopback sample, 1,000 keywords cost 3.0 ms instead of the ~350 ms the same writes cost one at a time.

Practical reading: choose V2, and enable EnableCache or a Preset if your workload is read-heavy. V2 with neither is the one configuration these numbers do not recommend.

Caveats

  • Loopback Redis has almost no network latency. Over a real network both schemas still pay one round trip on Find(), so V2’s larger payload matters more rather than less, and V1’s per-node Add() cost grows with the added latency.
  • These figures compare ACOR configurations against each other, not against other Aho-Corasick implementations. A single process with a static dictionary is better served by an in-memory library. ACOR earns its cost when several instances share one dictionary that changes at runtime.
  • Every other benchmark in the repository runs on miniredis, an in-process emulator with no round-trip cost. Those exist for regression detection and are deliberately not published here.