Status: causal gate closed on 2026-08-16. Controlled production experiments H-O9 through H-O33 are decided, and their accepted mechanisms are implemented. Direct Rust now passes the core Java-parity latency gate; provider traversal is within its resource/direct target; and the JVM result-delivery bottleneck has been reduced. The complete post-optimization cross-language matrix is now closed with exact results across all 45 shared coordinates. The follow-up structural wave also replaces the producer arena's read lock, queues compact generated-state IDs, rejects dictionary edges before materializing children, and isolates each foreign query's owner and fault channel.
This report explains the construction and matching gaps reported in the Java comparison. It separates three systems that were previously easy to conflate:
libdictenstein dictionary constructor;The conclusion is not that one allocator, garbage collection, or sorting is the culprit. The gaps are generated by different algorithms and ownership models. Rust performs substantially more structural work during construction, substantially more position/state work during matching, and then the resource path adds millions of callbacks, locks, handle materializations, and descriptor copies.
The machine-readable measurements behind every exact number below are in
observations.json.
The structural follow-up, including rejected alternatives, is in
structural-wave.json.
The diagnostic counters are compiled out unless perf-instrumentation is
enabled; timings and counters were therefore collected in separate runs.
The published anchor remains the committed 79,343-term aspell dictionary and
the 1,000-query standard/d2/std-d2 cell. Every native and resource run
returned the same result signature:
| field | value |
|---|---|
| matches | 18,514 |
| returned term bytes | 82,131 |
| summed distances | 36,201 |
| order-insensitive checksum | 7,775,666,136,087,164,888 |
The investigation adds deterministic 1k and 10k anchor strata plus 25k-term
prefix-heavy, suffix-heavy, and mixed-Unicode shapes, each in sorted and
seeded-shuffled order, and a full-anchor packed-u64 pair. The manifest labels
every cell's unit domain explicitly; runners do not infer semantics from file
names. generate_corpora.py records SHA-256 digests and basic prefix/suffix
structure. The sorted and shuffled members of a pair contain identical terms
and queries.
The evidence channels were deliberately redundant:
heaptrack_print;equals()-based sets.The reusable wrapper profile-headless.sh
forces Heaptrack's --record-only mode. It never invokes heaptrack -a,
heaptrack --analyze, or heaptrack_gui.
The Java SortedDawg.add bytecode rejects decreasing terms, finds the common
prefix with the previously inserted term, minimizes only the unchecked suffix,
and adds only the new suffix. That is the lexicographically ordered incremental
minimal-automaton construction described by Daciuk et al. [1].
Rust's DynamicDawg::from_sorted_terms does not implement that algorithm. It
only skips the input sort and then calls the ordinary persistent insert for
every term. Each insert:
ArcSwap graph revision;Arcs;The full anchor produces exactly one version load and one successful root publication per term, with no contention:
The algorithmic difference can be summarized without implementation detail:
ordered-minimal-build(sorted_terms):
previous := empty
for term in sorted_terms:
prefix := common_prefix(previous, term)
minimize_and_intern(unchecked_suffix_after(prefix))
append_only_new_suffix(term, prefix)
previous := term
minimize_and_intern(all_remaining_unchecked_nodes)
publish_root_once()
current-rust-build(terms):
for term in terms:
root := load_published_root()
revised_root := path_copy_and_insert(root, term)
compare_and_swap_published_root(root, revised_root)
reclaim_obsolete_path(root)
The first algorithm relies on sorted order to prove that the unchecked suffix will never be extended again. The second preserves concurrent incremental mutation semantics even when the entire input is already available.
| work item | count |
|---|---|
| term insertions / CAS publications | 79,343 |
| input units / path units / cloned edge lists | 673,918 |
cloned child Arcs | 2,948,511 |
| nodes created | 937,426 |
| nodes destroyed during the build | 753,261 |
| nodes remaining | 184,165 |
| CAS retries | 0 |
The identical path-copy counts for from_terms and from_sorted_terms prove
that current sorted input changes locality and preparation only; it does not
select a better construction strategy.
Thirty uninstrumented repetitions on CPU 3 give:
| isolated operation | median |
|---|---|
clone the in-memory String list | 2.417 ms |
| clone and unstable-sort it | 2.863 ms |
from_terms | 71.323 ms |
from_sorted_terms | 63.896 ms |
| sorted streaming inserts | 63.746 ms |
The sort itself adds only about 0.446 ms. The 7.43 ms from_terms premium
also includes cloning and later reclaiming all input strings. Avoiding that
work is useful, but a 63.9 ms persistent build still trails the 34.2 ms Java
median from the same profiling window. Sorting therefore cannot be the root
cause of the remaining gap.
In the current uninstrumented uProf capture, Rust construction self time is:
| hotspot | self time |
|---|---|
LockFreeDawgNode::drop | 36.65% |
insert_units | 28.80% |
SmallVec<Arc<Node>>::clone | 16.23% |
| ArcSwap debt payment | 4.71% |
malloc + free | 4.19% |
Heaptrack observes 4,466,530 allocations across one warm-up and two measured builds: about 1.489 million allocations per build, or 18.77 allocations per term. The dominant reclamation cost is not the allocator call itself. It is reference-count traffic plus discovering and destroying the obsolete persistent graph fragments.
This distinction matters for solution design. Deferring drops, changing the allocator, or moving reclamation to another thread can reduce producer latency, but those treatments retain the excess work and memory traffic. A bulk builder that never creates one published graph revision per term removes the cause.
With the Serial collector and a fixed 2 GiB heap, 50 Java builds incurred nine young collections totaling 93.812 ms. That is 4.68% of the summed timed build duration. GC pauses are a cost paid by Java, not enough hidden work to explain why it is faster.
Java does benefit from cheap bump allocation and batched reclamation, while
Rust performs synchronous Arc decrements and destructors. But the more
important fact precedes memory management: Java's ordered builder creates and
minimizes only the unchecked suffix, whereas Rust repeatedly creates and
destroys complete root paths. The GC conjecture is therefore refuted as the
primary cause and retained only as a secondary constant-factor effect.
The reproducible Java identity probe counts 29,133 physical nodes and 63,431
edges. Rust retains 184,165 nodes for the same terms: 6.32 times as many.
Rust's current compact hash-consing excludes final or valued nodes from
interning, so equivalent final suffixes cannot seed recursive equivalence.
For this corpus, the structure remains trie-shaped instead of becoming the
minimal acyclic word graph implied by the public terminology.
The structural matrix makes this visible without relying on English-word accidents:
| shape | sorted Rust | shuffled Rust | shuffled / sorted | live Rust nodes |
|---|---|---|---|---|
| full anchor | 64.07 ms | 108.03 ms | 1.69× | 184,165 |
| prefix-heavy 25k | 30.84 ms | 37.86 ms | 1.23× | 26,015 |
| suffix-heavy 25k | 29.76 ms | 38.22 ms | 1.28× | 326,003 |
| mixed Unicode 25k | 17.31 ms | 26.21 ms | 1.51× | 55,016 |
The suffix-heavy shape is the most diagnostic: 25,000 terms leave 326,003 nodes because the shared suffix is duplicated under different prefixes. Sorted Daciuk construction is designed precisely to intern such equivalent right languages while the unchecked suffix is still local.
| cause | confidence | role |
|---|---|---|
| per-term persistent path copying and root publication | very high | primary |
synchronous Arc reclamation of obsolete revisions | very high | primary consequence |
| no true sorted incremental minimization | very high | primary |
order-dependent edge-Arc cloning/locality | high | important for unordered streaming |
input String cloning | high | secondary |
| comparison sorting | high | minor |
| JVM garbage collection | high | not primary |
After H-O9 through H-O11 replaced per-term persistent publication with a private, freeze-once minimal builder, the same 30-repetition structural matrix was rerun with uninstrumented release binaries. The current full-anchor medians are:
| input and constructor | terms | median build |
|---|---|---|
unordered bytes, from_terms | 79,343 | 14.702 ms |
pre-ordered bytes, from_sorted_terms | 79,343 | 12.023 ms |
unordered packed u64, from_terms | 79,343 | 10.486 ms |
pre-ordered packed u64, from_sorted_terms | 79,343 | 5.186 ms |
| legacy Java ordered reference | 79,343 | 34.207 ms |
ordered incremental Rust insert stream | 79,343 | 67.165 ms |
The optimized unordered byte constructor is therefore 2.33× faster than the published Java ordered reference, and the explicit ordered constructor is 2.85× faster. The incremental stream remains intentionally distinct: it publishes an independently observable persistent revision per term and is not the bulk-construction path. The Java value was collected in the original parity window, not time-adjacent to this closing native matrix, so these ratios are closure indicators rather than paired causal estimates. The more important invariant is architectural: unordered and ordered construction now share the same unit-generic minimal-builder kernel, while the ordered API avoids sorting and the unordered API remains free to accept arbitrary input order.
The adversarial shapes remain informative after optimization. Prefix-heavy 25k input builds in 3.705 ms unordered and 1.119 ms ordered; suffix-heavy input builds in 16.149 ms and 13.708 ms respectively; mixed-Unicode input builds in 4.961 ms and 2.380 ms. The suffix-heavy case remains the hardest because its right-language equivalences demand the most registry work, but it no longer creates and reclaims a published root path for each term.
The pure Rust core is already behind Java before any foreign-language boundary. In the pinned standard/d2 profiling controls, Java's median was 427.7 ms per 1,000-query pass and Rust's was 769.1 ms, with identical result signatures. The absolute times include profiler/logging perturbation; their purpose here is to anchor the hotspot and work analyses, not replace JMH.
The uninstrumented Rust uProf self-time profile accounts for more than 84% in six named native functions:
| hotspot | self time |
|---|---|
transition_state_pooled_ref | 28.01% |
characteristic_vector | 27.00% |
QueryIterator::queue_children | 13.33% |
State::copy_from | 6.72% |
QueryIterator::advance | 5.27% |
State::insert_with | 4.20% |
Allocator entry points contribute only 2.58%. This corroborates the earlier profile: the gap is principally automaton/state work, not dictionary atomics or general-purpose allocation.
For one anchor pass, Rust attempts 6,996,242 dictionary-edge transitions. It first computes epsilon closure, then iterates every position in the expanded state and scans that position's query window to construct another characteristic vector:
| work item | count |
|---|---|
| epsilon-closure positions | 14,972,940 |
| characteristic-vector calls | 14,972,940 |
| query units inspected by those calls | 44,897,283 |
| successor candidates | 3,931,103 |
The Java path constructs the boolean characteristic vector for a dictionary edge and passes it into the state transition function. Rust's position-local windows make the same logical edge/state pair rescan overlapping query regions. The exact call identity and the 27% isolated hotspot make this a high-confidence primary cause.
Rust performs 5,535,780 state insertion attempts and 7,695,105 subsumption
checks. Every edge attempt calls State::copy_from; the anchor copies
14,972,940 Position values, or 359,350,560 bytes. Retained next states add
79,665,600 bytes to the pending queue.
Position is a general 24-byte representation and State is a sorted set of
positions. That generality supports all algorithms, but the common
standard-distance-small case pays comparison, insertion, subsumption, and
copy costs that a compact state ID, packed position, or generated transition
table could avoid. Schulz and Mihov's parametric Levenshtein-automaton work
[2] is the principled reference point, not a request for isolated loop
micro-tuning.
The pool handles the temporary epsilon-closure state and empty next states. A successful next state moves into the queue and is eventually dropped; it is never returned to the iterator's pool. The counters make the consequence plain:
| pool item | count |
|---|---|
| acquisitions | 13,992,484 |
| releases | 12,261,824 |
| misses | 1,728,311 |
| dictionary intersections | 1,731,660 |
Pool misses are within 0.2% of visited intersections. In effect, nearly every retained traversal state still requires fresh backing storage. A slot arena or pooled-state handle in the pending queue is a better ownership model than trying to enlarge the current temporary-state pool.
DynamicDawgNode::edges() clones its SmallVec<(label, Arc<Node>)> into an
owned vector and returns a boxed iterator. Heaptrack attributes 13,426,944
allocations to queue_children over four complete passes, about 3.357 million
per pass and approximately two allocations per visited node.
This is real avoidable work, but the latest uProf capture assigns little
self-time to the SmallVec clone and only 2.58% to allocator functions.
Borrowed/visitor-based edges are a sound experiment, especially for cache and
boundary work, but cannot alone close the Java gap.
| cause | confidence | role |
|---|---|---|
| position-local characteristic-vector recomputation | very high | primary |
| general position-set transition/subsumption representation | very high | primary |
| copying retained states into the traversal frontier | very high | primary |
| pool ownership gap for successful states | high | important consequence |
owned boxed edge enumeration and Arc cloning | high | secondary |
| byte versus Unicode unit domain | high | refuted on the ASCII anchor |
| dictionary atomics during reads | high | not a visible hotspot |
A clean production AMD uProf capture of the optimized
transposition/d2/tr-d2 path returned 18,524 matches, 85,208 term bytes,
36,314 summed distance, and checksum 84f045e4f4a37a73 on every repetition.
Its self-time distribution was:
| optimized hotspot | self time |
|---|---|
QueryIterator::advance | 45.55% |
transition_epsilon_closed_state_pooled_cached | 43.13% |
OSA State::insert_with | 7.55% |
| characteristic-cache lookup | 1.62% |
OSA means optimal string alignment, the adjacent-transposition distance implemented by this algorithm. This profile changes the next hypothesis: the old 27% characteristic-vector hotspot has been reduced to 1.62%, so further cache tuning is no longer the principal lever. Nearly 89% of self time now belongs to frontier advancement and the optimized transition kernel itself. A future campaign would therefore need a compact/generated small-distance OSA engine or a materially different frontier/state representation; isolated characteristic or allocation tweaks are no longer supported by the profile.
The structural wave removed the remaining ownership mismatch between the
generated transition table and the traversal frontier. Canonical position
slices now live once in the table, while queued built-in unit-cost
intersections carry a GeneratedStateId. The ordinary, ordered, priority,
ranked-value, value-filtered/value-yielding, and prefix-DFS schedulers share
that handle representation. Weighted, contextual, language-product, and
zipper-navigation states remain separate where an ID would either lose state
information or force materialization at the public navigation boundary.
One instrumented anchor pass contained 6,747,684 generated-table hits and
248,558 misses. Temporary transition-state pool misses fell to zero and the
hot traversal frontier no longer copies positional states. A checked sentinel
guard prevents an impossible ID from aliasing the EMPTY or UNCOMPUTED
transition-table values.
The label cache was then split into a hot classification path and a cold
pattern path. Direct byte labels and overflow labels retain only a compact
class ID; one central class table owns each characteristic pattern. H-O32
reduced the resource-path median from 204.104 ms to 190.242 ms across 54
samples per arm (6.79%, Welch p = 1.323e-11, Cohen's d = -1.657) and was
retained. Two superficially more compact layouts were rejected after exact
result-preserving measurements: the first flat generated-transition prototype,
which retained the old growth and lookup shape, regressed 153.245 ms to
179.958 ms; structure-of-arrays foreign edges regressed 189.383 ms to
199.141 ms. The later accepted dense target table in § 3.8 changed the row
layout and widening invariant rather than reviving that rejected prototype.
The closing independent pure-Rust harness measured 138.927 ms across 51
samples (range 136.889–141.828 ms), compared with 382.407 ms across 30 legacy
Java samples (range 380.230–387.886 ms). Both returned the exact anchor
signature. Rust is therefore 2.75× faster on the original
standard/d2/std-d2 workload; this is a direct-core comparison and should not
be conflated with the JVM binding matrix discussed below.
The next headless AMD uProf pass separated two residual transition regimes. Eligible short Standard queries spent repeated work maintaining cumulative edit-budget lanes and recomputing already-seen packed frontiers. The other three unit-cost algorithms still used positional frontiers whose cached target rows were separately allocated and pointer-chased.
For Standard, PackedEditLaneLayout now stores positions at one exact edit
cost per lane. One-to-three-query-deletion closure is the fixed expression
T = I \mathbin{\lor} \bigvee_{j=1}^{k}
\left((I \mathbin{\land} D_j) \ll j(w+1)\right),
where I is the consuming-transition frontier, k is the maximum distance,
w is the lane width, and D_j masks the positions from which exactly j
deletions remain valid. Every shifted term reads from I, so the kernel avoids
the cumulative-lane promotion chain. The production-only comparison reduced
the direct Unicode Standard distance-2 median from 77.703 ms to 72.379 ms
(6.852%) across 51 alternating pinned samples. Distance controls were d0
+0.154%, d1 -7.156%, and d3 -5.968%; exhaustive tests cover distances 0–3,
all three unit domains, and the bit-63 boundary.
The packed frontier is then interned lazily as a dense state identifier. A
row-major table indexed by (state, exact-label-class) stores the next state.
Only states reached by the concrete dictionary walk are constructed. The
Standard distance-2 median fell from 85.694 ms to 80.427 ms (6.146%); an
instrumented run observed 6,748,273 table hits and 247,969 misses, a 96.46%
hit rate.
For the positional engine, DenseGeneratedTargets retains canonical position
slices in stable boxes but stores every target in one row-major allocation. A
power-of-two row width grows losslessly only if a substitution policy creates
more label classes than the query-length-derived initial bound. This reduced
Transposition distance-2 from 132.211 ms to 125.787 ms (4.858%),
Merge-and-Split distance-2 from 758.19 ms to 718.33 ms (5.26%), and
unrestricted Damerau distance-2 from 154.76 ms to 135.17 ms (12.66%). A
follow-up u32 target encoding improved only 0.182%, missed its 3% gate, and
was reverted. The result supports dense locality, not narrowing IDs at the
cost of checked conversions.
The resource path performs exactly the same core logical query work and returns the same checksum as the direct Unicode Rust control. On top of that work, one 1,000-query pass performs:
| boundary operation | count |
|---|---|
| traversal snapshots | 1,000 |
| arena mutex acquisitions | 3,500,348 |
| finality calls | 1,750,174 |
| edge calls and cold edge-cache misses | 1,731,660 each |
| native edges enumerated / descriptors cloned | 6,996,242 each |
| node handles materialized | 6,997,242 |
Each query creates a new TraversalSnapshot, so its edge cache starts empty.
Every visited node crosses a vtable callback and mutex. Every child becomes a
new arena handle, and the edge descriptor vector is copied back to the
consumer. This scales with traversal work—millions of edges—not with the
18,514 matches.
Batch sizes 1, 256, and 65,536 produce identical core, callback, lock,
materialization, and descriptor counts. Only the number of nonempty result
batches changes. Therefore the earlier hypothesis that C wins because it
avoids per-match strings or because result batching is the dominant cost is
refuted. The C/resource path does allocate a Rust String before copying
bytes into ABI-owned output, and prior uProf runs place snapshot edge
materialization—not result packing—at the top of its boundary-specific work.
The binding gap should consequently be decomposed into two experiments:
Changing only result batch size tests the second while leaving the first untouched, which is why it cannot diagnose the observed gap.
These were the causal report's preregistered design directions. Section 6 records which concrete treatments survived measurement. They remain here so the implemented mechanisms can be traced back to their causal predictions.
Offline sorted minimal builder. Make from_sorted_terms build a local
mutable unchecked suffix, intern equivalent right languages as terms
arrive, and publish one immutable DynamicDawg root at the end. Acceptance
signals: the anchor reaches the Java identity-node count, construction
eliminates per-term root CAS/drop, and the suffix-heavy shape collapses.
Fast unordered bulk builder. Let from_terms collect terms and delegate
to the minimal builder after byte/radix sorting. Compare this with an
unordered arena-trie plus bottom-up acyclic minimization. Sorting is only
0.45 ms on the anchor, so lower peak structure and one-pass minimization are
likely more important than avoiding sorting at all costs.
Preserve true incremental mutation separately. insert should retain
its concurrency semantics. Bulk construction should not pay for them.
A type-compatible constructor can build privately and publish once; a
separate immutable/static graph type is warranted only if node ownership
or layout cannot satisfy DynamicDawg's mutation contract cleanly.
Owned-input constructor. An overload that consumes Strings can remove
the measured 2.42 ms clone cost. It is complementary to the builder change,
not a substitute for it.
Edge-level characteristic mask. Compute one compact mask for the dictionary edge and maximum relevant query window, then extract the view needed by each position. The preregistered prediction should target the 27% hotspot and the 44.9 million inspected units, not merely wall time.
Parametric/generated small-distance engine. For standard distance 0–2, represent automaton states as compact IDs and transitions as a function of state ID plus characteristic mask. Keep the general position-set engine for richer algorithms and larger bounds. This directly attacks transition, subsumption, copying, and representation overhead together.
Frontier-owned pooled slots. Store a pool/arena slot ID in each pending intersection and return the state when the intersection is popped. The prediction is that misses fall from approximately one per visited node to a bound near peak queue occupancy.
Borrowed edge visitation. Add a visitor or generic associated iterator
path for native nodes so traversal does not allocate a boxed iterator or
clone child Arcs. Treat it as a secondary experiment with allocation-count
and cache-miss criteria.
The boundary gate must continue recording callbacks and descriptors. A faster wall time with unchanged millions-scale boundary counts is likely a constant-factor improvement, not closure of the architectural cause.
Every treatment was opened in pgmcp before measurement. Timed arms used uninstrumented release binaries; mechanism counters, AMD uProf, and headless Heaptrack were separate diagnostic runs. A statistically significant result did not override a preregistered practical-magnitude gate. Rejected source changes were reverted while their experiment records remain immutable.
| ID | single treatment | median result | campaign decision | causal interpretation |
|---|---|---|---|---|
| H-O9 | freeze-once sorted minimal DAWG builder | 64.119 → 16.972 ms, 3.778× | retain | Per-term persistent publication and reclamation were primary; the retained graph reached Java's 29,133 physical nodes |
| H-O10 | sort once, then use the same minimal builder for unordered input | 67.214 → 19.805 ms, 3.394× | retain | Sorting was inexpensive enough that unordered bulk input should share the ordered kernel |
| H-O11 | FxHashMap for the private merge registry | 16.986 → 11.085 ms, 1.532× | retain | Randomized hashing was a large constant factor after the algorithmic repair |
| H-O12 | inline four-edge merge signatures | 10.990 → 11.753 ms, 6.95% slower | reject and revert | Larger hash-table keys and key moves cost more than the removed small allocations |
| H-O13 | generic borrowed edge visitor | 765.688 → 665.948 ms, 13.03% lower | retain | Owned vectors and boxed iterators were important secondary traversal work |
| H-O14 | unit-generic characteristic cache | 669.995 → 527.319 ms, 21.30% lower | retain | Repeated position-local query scans were a primary cause |
| H-O15 | bulk contiguous state copy | 623.995 → 565.961 ms, 9.30% lower | retain | Scalar position copying was measurable and representation-independent |
| H-O16 | enqueue epsilon-closed states exactly once | 497.098 → 320.098 ms, 35.61% lower | retain | Repeating label-independent closure for every sibling edge was the largest isolated native cost |
| H-O17 | labels first, materialize accepted children later | 39.898 → 38.835 ms, 2.66% lower | reject and revert | A second accepted-edge lookup consumed most of the avoided child-handle traffic and missed the 15% gate |
| H-O20 | accumulated-cost guard before subsumption dispatch | 4.722% lower; checks only 1.65% lower | reject and revert | It missed the 5% engineering gate and did not remove meaningful structural work |
| H-O21 | batch-sort and normalize raw successors | 317.033 → 352.388 ms, 11.15% slower | reject and revert | Sorting tiny candidate sets cost more than the 4.72% reduction in dispatched comparisons |
| H-O28 | freeze-build an empty binding-owned dictionary from one validated batch | 96.271 → 26.547 ms, 3.626× | retain | One foreign call had still executed 79,343 incremental insertions internally; the treatment routes byte, Unicode-scalar, and packed-u64 batches through the same unit-generic minimal builder |
| 238 | production exact-cost packed lanes | 77.703 → 72.379 ms, 6.852% lower | retain | Exact-cost lanes and closed-form deletion closure remove cumulative-budget promotion from every eligible Standard transition |
| 242 | lazy compact packed DFA | 85.694 → 80.427 ms, 6.146% lower | retain | Reached packed frontiers and exact label classes form a 96.46%-hit dense query-local transition table |
| 245 | flat dense positional target table | Transposition 132.211 → 125.787 ms, 4.858% lower | retain | One row-major target matrix removes per-state target allocations and pointer chasing; Merge-and-Split and true Damerau improved 5.26% and 12.66% |
| 247 | encode positional targets as u32 | 126.388 → 126.157 ms, 0.182% lower | reject and revert | Smaller targets did not repay checked conversion and missed the 3% practical gate |
H-O2's proposed public Position size of at most eight bytes was cancelled at
the feasibility gate rather than implemented lossily. The public structure
contains two independently lossless usize fields, already requiring sixteen
bytes on the supported 64-bit target before the position kind and auxiliary
payload. The API also exposes &[Position] and accepts full-range inputs, so a
hidden narrow representation would either break the contract, truncate valid
values, or require an overflow representation larger than the target. H-O16
simultaneously reduced measured queued-state copy calls, positions, and bytes
to zero, invalidating H-O2's stated performance mechanism. This is a resolved
negative feasibility result, not deferred implementation work.
H-O18 and H-O19 applied the same causal method to the double-array trie (DAT) rather than forcing DAWG construction logic onto a different representation. A generic two-phase static DAT builder removed incremental collision relocation; one outer shared node handle then removed four redundant atomic increments and decrements per child traversal, reducing its query median by 27.72%. The public incremental builder remains available for genuinely dynamic construction.
H-O28 closed the corresponding binding construction gap. Its 51-sample control
and treatment distributions were completely separated (Mann–Whitney
p = 4.83e-73, Cohen's d = -13.90, Cliff's delta = -1). The safe Rust
batch operation validates the whole input before publishing, so it remains
atomic on failure. The established C ABI instead promises that the valid
prefix preceding a malformed descriptor remains applied; its error path
therefore freeze-builds precisely that validated prefix before returning the
original descriptor error. Successful empty batches publish one graph in both
interfaces, while nonempty dictionaries retain incremental update semantics.
| ID | single treatment | median result | campaign decision | mechanism result |
|---|---|---|---|---|
| H-O22 | stream validated ABI edge pages into the generic visitor | 887.303 → 841.661 ms, 5.144% lower | retain | Removed consumer aggregate vectors and boxed iteration; boundary counts intentionally unchanged |
| H-O23 | explicitly pin one immutable resource snapshot | 841.661 → 654.813 ms, 22.20% lower | retain, 25% magnitude clause missed | Snapshot count became one; cache misses fell 96.61% and node materializations 98.74% |
| H-O24 | provider copies only the requested borrowed edge page | 654.813 → 632.885 ms, 3.35% lower | retain, 5% magnitude clause missed | Whole-vector descriptor clones fell from 6,996,242 to zero |
| H-O25 | optional fused finality-and-edge ABI visit | 632.885 → 588.835 ms, 6.96% lower | retain, 10% magnitude clause missed | Arena locks halved from 3,500,348 to 1,750,174 and standalone finality callbacks became zero |
| H-O26 | cache validated nodes by immutable snapshot node ID | 588.835 → 473.051 ms, 19.66% lower | retain, 20% magnitude clause missed | Callbacks fell 96.61%, locks fell 95.59%, and the contemporaneous resource/direct ratio reached 1.132× |
| H-O27 | reduce borrowed JVM descriptors instead of materializing Match/String objects | 68.994 → 69.078 ms, 0.12% slower | reject as parity default | Repeated foreign-memory descriptor access cost at least as much as the removed allocations; this timing alone does not establish whether scalar replacement contributed |
| H-O30 | drain every query through one confined foreign-memory arena and a lexical forEachMatch callback | 68.994 → 58.327 ms, 15.46% lower | retain | Per-query shared arenas had forced 68,001 JVM all-thread handshakes; the lexical path required two |
| H-O31 | replace each cached foreign child pointer with only its numeric node ID | 200.549 → 284.595 ms, 41.91% slower | reject and revert | Re-resolving accepted children through the hybrid directory cost more than the pointer saved on every edge |
| H-O32 | store only characteristic class IDs in direct and overflow label caches | 204.104 → 190.242 ms, 6.79% lower | retain | Removed duplicate pattern ownership and kept full pattern access on the cold generated-table miss path |
| H-O33 | split cached foreign labels and child metadata into parallel arrays | 189.383 → 199.141 ms, 5.15% slower | reject and revert | Extra indexing and lost edge-record locality outweighed denser label scanning |
| 239 | direct immutable resource snapshot graph | 118.325 → 85.916 ms, 27.39% lower | retain | One validated compact graph removes steady-state node callbacks, consumer cache-directory lookup, and atomic child promotion |
The retained boundary treatments compose because they remove different work:
snapshot construction, provider-side descriptor cloning, redundant property
callbacks, repeated inspection of an immutable node, and per-query foreign
arena cleanup. Their generic
contracts and backend applicability are inventoried in
optimization-propagation.md.
The follow-up resource architecture additionally separates one retained
query-local provider owner from copy-only node keys, isolates callback faults
per cursor, and keys shared immutable caches by snapshot identity plus provider
and dictionary vtable lineage. Its dense-prefix/sparse-overflow directory is
lock-free on lookup and publication, bounds dense allocation, and immediately
reclaims losing publications. Failed producer-arena growth does not consume an
ID, so capacity failure cannot leave a permanently unpublishable hole. These
changes close concurrency defects as well as removing millions of Arc
increments/decrements and lock acquisitions.
The follow-up snapshot implementation removes three sources of work that were
still hidden behind the accepted H-O23 through H-O26 treatments. First, a
producer memo retains one immutable snapshot per source revision. Second, the
optional vt.snapshot.id.1 interface identifies that revision as the pair
(producer, revision), allowing separately minted resource contexts to share a
consumer node cache without equating mutable resources. Third, the producer
arena is a 256-slot chunked, append-only directory: readers use an atomically
published chunk vector and write-once slots, and growth publishes a fallibly
allocated geometric directory with compare-and-swap. No arena operation takes
a mutex. The last arena owner reclaims all chunks synchronously; no
background reclaimer or unbounded deferred queue is involved.
The same 79,343-term, 1,000-query standard/d2/std-d2 causal workload retained
the exact result signature (18,514 matches, 82,131 returned term bytes,
distance sum 36,201, checksum 7,775,666,136,087,164,888). Its provider work
changed as follows relative to the original resource-boundary observation:
| causal work | original boundary | hardened stack | reduction |
|---|---|---|---|
| snapshots created | 1,000 | 1 | 99.90% |
| arena mutex acquisitions | 3,500,348 | 0 | 100% |
| provider edge callbacks/cache misses | 1,731,660 | 58,677 | 96.61% |
| native edges enumerated | 6,996,242 | 88,326 | 98.74% |
| nodes materialized | 6,997,242 | 88,327 | 98.74% |
| descriptor clones | 6,996,242 | 0 | 100% |
These are causal work counters, not a new latency experiment: the counter build is instrumented and the uProf run is a single diagnostic sample, so its timing must not be compared with the uninstrumented 51-sample medians above. A headless AMD uProf 5.3 hotspots capture instead answers the mechanistic question. After the structural wave, predicate-first foreign-edge processing accounted for 30.26% self CPU, generated transitions for 21.49%, cursor advance for 11.40%, and the epsilon kernel for 5.70%. Dense arena lookup and growth each accounted for 0.44%; no arena lock remained. The profile therefore corroborates the counter result: the remaining work is primarily query-transition and frontier work, not repeated snapshot construction or a global producer-arena lock.
A separate teardown-aware counter run released every producer and consumer
owner after stopping query_ns. It synchronously reclaimed all 88,327
materialized nodes in 33.61 ms on that diagnostic run. The equality between
materialized and reclaimed counts is the bounded-lifetime gate; the single
latency observation is diagnostic rather than a distributional performance
claim.
The warmed callback arena was still an indirect representation of an already
immutable DynamicDAWG revision. The accepted successor exposes one optional
vt.dict.graph.v1 view: dense node descriptors, sorted flat edges, and opaque
value cursors. Producer and consumer each publish their derived representation
once per (producer, revision) identity. Snapshot capture remains
$\mathcal{O}(1)$; only the first graph request pays
$\Theta(\lvert V\rvert + \lvert E\rvert)$ projection and validation, outside
the backend and registry locks.
On the same committed Standard distance-2 workload, with only
resource-profiling controls enabled and hot-loop counters compiled out, nine
runs gave these exact medians:
| resource traversal | median for 1,000 queries | relative to callback fallback |
|---|---|---|
| identity-cached callback/page fallback | 147.825 ms | control |
| immutable flat graph | 85.639 ms | -42.07% |
Every run retained 18,514 matches, 82,131 returned bytes, distance sum
36,201, checksum 7,775,666,136,087,164,888, and order checksum
16,014,396,901,440,918,890. The instrumented work run explains the change:
one graph projection, one consumer decode, zero arena locks, zero finality or
edge callbacks, and exactly one graph-value call per returned match. The
callback control made 22,514 finality calls and edge-page calls, copied
53,227 edge descriptors, and reclaimed 26,068 materialized handles; the
graph path reclaimed only its root fallback handle.
The same graph format now packs node finality into the high bit of its existing
64-bit edge-range word. This preserves the eight-byte hot descriptor and
removes the separate finality allocation and load for every automaton that uses
TraversalSession. A before/after diagnostic moved the median from 85.639 ms
to 82.076 ms (-4.16%) with the exact signature unchanged. The raw producer ABI
also uses checked one-based dense value tokens: zero, out-of-range, and forged
cursors fail before any backend pointer is touched.
The product-reuse trace motivated a bounded lossy-cache investigation rather than an unbounded memo table. TinyLFU and W-TinyLFU use approximate frequency sketches and admission to resist one-hit pollution [3]; related low-mutation policies include CLOCK-Pro [5], S3-FIFO [6], and SIEVE [7]. Those algorithms are most valuable when a miss is expensive and frequency predicts saved work. Neither condition held here.
Across 16,807,209 product expansions, 4,557,624 products repeated, but only
572,729 maximum-capacity hits (3.4% of all products) had outgoing edges.
87.3% of captured hits were zero-edge leaves, and the avoided transitions
were already cheap generated-table hits. Most decisively, every one of the
433,681 expensive generated-transition misses remained a miss. A full 4 MiB
TinyLFU prototype observed 99.3% of repeated keys yet slowed the workload
because sketch hashing and counter updates exceeded recomputation cost. The
smallest application-specific alternative—a 64 KiB, degree-at-least-four,
two-way approximate-LFU table—achieved only a 0.36% hit ratio and avoided
40,813 edge scans, also insufficient to pay for lookup.
This is a workload conclusion, not a claim that approximate LFU is ineffective in general. Count-Min sketches provide a sound fixed-space frequency estimator [4], but frequency estimates cannot recover value when the frequent records are cheap leaves. Production therefore retains no second- level product cache. The accepted query-local transition tables already cache the expensive recurrence at its semantic key, with constant bounded state and no eviction policy.
The complete-query cache is a different reuse boundary. Its miss cost is the
entire dictionary/automaton product walk and its keys recur across calls, so
frequency can predict substantial saved work. VersionedQueryCache<V> uses a
four-row packed 4-bit frequency sketch and two-probe doorkeeper for TinyLFU
admission, with periodic aging, plus one SIEVE reference bit per resident for
low-mutation victim selection. Entry count and caller-defined logical weight
are independent hard bounds; hash collisions still compare the complete query
text and distance; distance variants share one Arc<str>; and an immutable
result slice is shared only after exact computation. A dictionary-version
change clears both residency and policy. The cache is deliberately
single-owner and synchronization-free so callers can shard ownership without
paying for a lock or atomic protocol on every hit.
The final topology-gated, 51-replicate, 128-entry policy matrix separates scan resistance, adaptation, Zipf locality, and policy overhead:
| policy | hot entries after scan | rounds to 95% in a disjoint phase | phase hit rate | Zipf hit rate | Zipf ns/op |
|---|---|---|---|---|---|
| FIFO | 0 | 1 | 0.984375 | 0.549617 | 29.288 |
| LRU | 0 | 1 | 0.984375 | 0.603128 | 29.159 |
| SIEVE alone | 0 | 1 | 0.984375 | 0.615622 | 27.275 |
| aging exact LFU | 127 | 25 | 0.609619 | 0.661372 | 199.165 |
| TinyLFU + SIEVE | 128 | 19 | 0.837402 | 0.664172 | 61.409 |
TinyLFU + SIEVE is the only candidate that retained the complete hot set under
the one-hit scan while adapting faster than exact LFU and also producing the
best Zipf hit rate. It cut exact-LFU Zipf policy cost by 69.17%; pgmcp
experiment 299 accepted that preregistered comparison (p = 2.19e-112, 51
samples per arm). SIEVE alone remains a useful latency floor, but its 34.134
ns/op advantage ceases to cover its lower hit rate when a miss costs more than
approximately 703 ns. The complete matrix, host ledger, and interpretation are
in the policy report; the primary
machine-readable evidence is
query-cache-policy-performance.csv.
The first allocation-reuse timing was invalidated rather than rationalized. It gave each process an independent randomized AHash key and its treatment moved the SIEVE hand and cleared reference bits even when admission was rejected, whereas the allocating reference rolled the transaction back. The preserved invalid file is named explicitly in the evidence directory.
The corrected planner encodes the transaction as a circular span instead of per-slot generation marks. The first pass considers every unreferenced resident and virtually clears referenced residents; a second pass can consider only those formerly referenced residents. Every live resident has therefore been selected or rejected after at most two passes. Rejection discards only a length and leaves the hand and reference bits untouched. Successful admission replays the first-pass span once to commit reference clears, then removes the small victim list. This removes allocation, two per-slot mark arrays, and the rejected-path write log without weakening rollback semantics.
In 51 alternating, CPU-3-pinned pairs with a fixed benchmark-only AHash seed
and topology admission before every pair, the allocating transactional
reference averaged 1,553,788 ns and the circular-span planner averaged
1,227,706 ns: a 20.99% reduction. The paired mean delta was -326,082 ns with a
95% confidence interval of [-329,649, -322,516] ns (p = 2.05e-72, paired
d_z = -25.71). Every pair agreed on checksum, hot-set retention, resident
entries and weight, hits, misses, admissions, rejections, and evictions. The
timings
and host-load ledger
are retained together.
Character-phonetic queries have two mutually exclusive engines. Unit-cost
queries use incremental dictionary/language-product intersection; articulatory
costs require the fractional-cost product scanner. The former iterator used to
retain the latter engine's cloned ProductAutomatonChar, empty queue, optional
traversal session, capacity-64 parent-path arena, and depth guard even though
none could be observed. A query-lifetime enum now owns exactly one engine. The
value-returning mapped iterator uses the same split and routes incremental
matches through MappedLanguageQueryIterator, so compact cursor-native graph
traversal is shared instead of reimplemented.
The causal control is a separate benchmark-only concrete type with the exact
historical field construction and drop order. Arm selection occurs once before
the measured loop, and first-result/full-order checksums are computed outside
that loop; consequently the production iterator contains no control Option
and the result check does not become part of the treatment.
On the current post-propagation binary, 51 topology-admitted alternating pairs
of 1,000,000 construct/drop iterations reduced mean time from 1,061,038,836 ns
to 717,876,938 ns (-32.34%). The paired mean delta was -343,161,898 ns with a
95% confidence interval of [-346,665,371, -339,658,426] ns
(p = 6.55e-74). Every pair retained identical first-result and full-consume
match, byte, distance, checksum, and order signatures. Inline iterator size
fell from 712 to 432 bytes.
Fresh headless Heaptrack captures of 10,000 simultaneously live iterators on
the same binary reproduced 676,412 to 626,396 allocation calls, 223.79 MiB to
191.07 MiB reported peak heap, and 127.15 MiB to 93.92 MiB reported peak RSS.
Both arms used heaptrack --record-only followed by heaptrack_print; no GUI
analyzer was launched. The timing CSV,
timing host ledger,
Heaptrack summary,
and Heaptrack host ledger
are retained together.
After graph capture and lock removal, instrumented resource traversal recorded zero provider-arena acquisitions. Its remaining Standard distance-2 work was 6,996,242 logical packed transitions, of which 4,400,619 labels belonged to class zero (absent from the query) and 3,083,387 repeated class zero within the same dictionary-node expansion. A source-row-local exact result slot therefore reuses both live targets and the dead sentinel without hashing, allocation, or growth. Across 51 topology-admitted same-binary pairs it reduced physical DFA target probes from 6,996,242 to 3,912,855 (-44.07%) and mean query time from 192.622 ms to 189.100 ms (-1.83%). The paired median reduction was 1.91%, the 95% confidence interval for the mean delta was [-4.509, -2.535] ms, and every result and logical-work signature was identical. The more invasive earlier attempt to broadcast class-zero results through a separate expansion grouping was rejected because its bookkeeping made the same workload 15.16% slower.
Schedulers that own a representation-erased UnitCostMachine formerly
matched that enum again in every sibling transition. A crate-private
PreparedUnitCostRow<U> seam and centralized macro now select Standard, OSA,
merge/split, or positional rows once per dictionary node; the expansion body
is monomorphized for the concrete packed type and has no vtable. Fifty-one
same-binary pairs reduced mean resource query time from 180.569 ms to 175.904
ms (-2.58%), with a 2.51% paired-median reduction and a mean-delta confidence
interval of [-5.548, -3.783] ms. Exact result, transition, graph, value, and
zero-lock counters agreed in every pair. The same seam is used by value,
ranked-value, ordered, and priority schedulers; the direct query kernel already
performs equivalent query-lifetime concrete selection.
Finally, the generic append-only reconstruction arena used machine-word keys
and depths even though one query cannot approach that space. Checked u32
keys and depths reduce ParentPathKey from eight to four bytes and the common
ParentPathNode<char> from 24 to 12 bytes, reserving u32::MAX as the root
sentinel and rejecting exhaustion. Fifty-one alternating, digest-recorded
two-binary pairs reduced mean query time from 99.352 ms to 96.873 ms (-2.50%);
the paired-median reduction was 2.72% and the confidence interval for the mean
delta was [-2.899, -2.061] ms. Headless Heaptrack reported identical 744,467
allocation calls, 249,845 temporary allocations, and 672 leaked bytes; peak
heap moved from 18.63 MiB to 18.59 MiB and peak RSS from 27.58 MiB to 27.35
MiB. The class-zero timings,
static-dispatch timings,
parent-path timings,
and headless allocation summary
are retained with adjacent host-load ledgers.
A closing headless AMD uProf capture after these changes measured 1.119 s for 15 complete 1,000-query resource passes (74.62 ms/pass). No arena lock appears. The remaining named costs are the packed-DFA class-zero/cache branch (9.13%), flat-graph edge projection (8.70%), queue movement, parent-path insertion, and final-distance calculation, all individually single-digit percentages. This marks the transition from architectural bottlenecks to micro-optimization: further changes require their own causal controls and should not reintroduce grouping, hashing, synchronization, or unbounded memoization merely to remove a predictable branch or array probe.
H-O30 generalized the lexical drain across string, byte, packed-u64, and
phonetic query forms. One confined arena now owns the whole synchronous drain,
ordinary owned Match values cross the public callback, and cleanup is
deterministic even when the consumer throws. Java Flight Recorder showed why
this was faster: the former shared-arena/Cleaner path triggered 68,001
HandshakeAllThreads VM operations, totaling approximately 276 ms in the
diagnostic run, while the lexical path triggered two totaling approximately
0.019 ms. Its 51-sample control and treatment distributions were completely
separated (one-sided Welch p = 1.50e-26, Cohen's d = -3.69,
Mann–Whitney p = 1.65e-18, Cliff's delta = -1).
The allocation evidence also refutes garbage collection as the explanation for legacy Java's query lead. In the paired diagnostic workload the optimized Rust-backed JVM path allocated 2,966,440 bytes per operation, whereas legacy Java allocated 150,576,544 bytes per operation—about 50.8× more. Legacy still ran faster before the native and boundary treatments, so allocation volume and garbage collection cannot be the primary causal mechanism.
The completed 60-cell post-H-O26 resource/direct matrix covers every supported
algorithm at distances 1–3 and both in-vocabulary and out-of-vocabulary query
shapes. All 60 cells preserved match count, returned term bytes, summed
distance, and checksum exactly. The resource/direct latency-ratio geometric
mean was 1.187× and the median was 1.180×, passing the aggregate 1.35× gate.
Individual cells ranged from 1.064× (Damerau distance 2, out-of-vocabulary) to
1.453× (merge-and-split distance 3, std-d3). The isolated worst cell is
retained as a tail target rather than hidden by the aggregate result.
This section preserves the 2026-08-15/18 closure state and its rejected experiment as historical evidence. Section 6.5 is the definitive breadth measurement for the final binary; none of its later results are projected back onto the binaries measured here.
A fresh pure-language closing run on 2026-08-18 isolates the Rust core from
the JVM binding. Both arms were pinned to CPU 0 and admitted before and after
measurement against the complete shared LLC group (CPUs 0--7); unrelated work
on other LLC groups was recorded but did not invalidate the pair. For the
standard/d2/std-d2 1,000-query pass, pure Rust measured 86.384 ms median
(MAD 0.239 ms, 51 samples) versus 456.396 ms for legacy pure Java (MAD 10.448
ms, 51 samples): Rust is 81.07% lower latency, or 5.28x faster. Every sample
had the identical 18,514-match, 82,131-byte, distance-sum-36,201 signature and
checksum 6be8b7274d8277d8. Construction from the same pre-sorted 79,343-term
in-memory list measured 14.751 ms for Rust (MAD 0.271 ms) versus 66.881 ms for
legacy Java (MAD 21.384 ms), ten in-process builds each: Rust is 77.94% lower
latency, or 4.53x faster. The large Java construction MAD is reported rather
than filtered; it is consistent with JIT and collection variability and does
not alter the direction of the result. The committed
closing summary
and retained raw distributions replace the earlier contaminated Rust-only
attempt, whose post-run LLC gate failed and which is explicitly quarantined.
This resolves the original native comparison: optimized liblevenshtein-rust
is no longer slower than liblevenshtein-java in either construction or
matching on the parity workload. Any remaining JVM-to-JVM deficit is therefore
binding, cursor-delivery, and managed-materialization overhead, not a slower
Rust dictionary or transition engine.
The post-H-O16 direct Rust standard/d1/hits median was approximately
39.976 ms per full 1,000-query pass. The final H-O7 compiler-guarded rerun
measured 42.057 ms across 51 samples, with a bootstrap median interval of
[41.959, 42.396] ms, compared with the legacy Java parity threshold of
51.2 ms. The direct core is therefore 17.9% faster at the closing gate and
passes the epic's core latency threshold. The optimized JVM resource path, in
the same-build H-O27 materialized control, measured 68.994 ms over three JMH
forks and 51 measurement iterations. H-O30 subsequently reduced the same
managed-materialization path to 58.327 ms by changing arena lifetime and drain
shape, a 15.46% reduction. Borrowed result reduction measured 69.078 ms and was
rejected (p = 0.820, Cohen's d = 0.182; robust
Mann–Whitney p = 0.862). The public borrowed API remains useful when a caller
needs lazy decoding or explicit allocation control, but the parity harness
keeps managed materialization because it is the faster measured path and
matches the cross-language sample contract. The remaining JVM gap is therefore
Java foreign-function-interface and cursor-delivery work layered on an
already-faster Rust core, rather than the original native transition gap.
The unchanged closing matrix contains 45 paired coordinates and 90 timed cells. All 45 pairs agree exactly on match count, returned term bytes, summed distance, and checksum. The geometric mean of legacy time divided by Vinary time is 0.965, the median is 0.974, and the range is [0.766, 1.109]. Vinary wins 16 coordinates and legacy wins 29. By algorithm, Vinary is 1.8% slower on the standard geometric mean, 1.9% faster on transposition, and 11.2% slower on merge-and-split. Practical parity is therefore achieved overall, while the preregistered requirement of at least 3x Vinary throughput is not achieved.
The original standard/d1/hits coordinate now measures 54.543 ms through the
Vinary JVM binding versus 51.256 ms for legacy, a 6.4% residual. At the H-J1
transposition/d2/tr-d2 coordinate, the breadth run measures 445.499 ms for
Vinary versus 448.694 ms for legacy, a 0.7% Vinary lead. The breadth matrix is
the unchanged workload closure. The formal 51-sample pair measured 466.581 ms
for Vinary (bootstrap median interval [463.029, 468.978] ms) and 455.861 ms for
legacy ([455.223, 456.856] ms). Vinary was 2.35% slower; the one-sided Welch
test gave p = 0.664 and Cohen's d = +0.084. Experiment 205 is rejected both
statistically and by its mandatory practical clause requiring Vinary median
latency no greater than one third of legacy. The exact signature remained
18,524 matches, 85,208 returned bytes, distance sum 36,314, and checksum
84f045e4f4a37a73 in every fork.
The final direct-language gate strengthens §6.3 with 51 independent processes
per arm, alternating pair order and admitting each process immediately before
and after timing against CPU 3 and its complete LLC group. Seven attempted
query admissions were rejected and excluded. Every accepted
standard/d1/hits sample returned the same 3,620 matches, 24,726 bytes,
distance sum 2,620, and checksum 3bdc59281f42611a:
| direct query arm | median pass | 95% bootstrap CI | median per query |
|---|---|---|---|
| optimized pure Rust | 14.135 ms | [14.113, 14.185] ms | 14.135 µs |
| legacy pure Java | 46.759 ms | [46.496, 47.283] ms | 46.759 µs |
The intervals do not overlap. Legacy/Rust median latency is 3.308×, Rust is
69.77% lower, the pooled Cohen effect is -24.292, and the paired median
difference is -32.600 ms. The preregistered post-optimization experiment was
accepted after correcting its arm-label mapping from generic
control/treatment names to the recorded legacy_java/pure_rust labels;
the criterion and all samples were unchanged. The accepted Welch result is
t = -122.666, p = 1.11e-66, with Cliff's delta -1. This is the definitive
closure of the epic's 51.2 µs/query native gate: Rust is 72.4% below the
threshold and every Rust sample is faster than every Java sample.
The corresponding construction pair measures cold, one-shot construction:
each sample is a fresh process, and the Java arm deliberately receives no
in-process builder warmup. That answers the common build-once workload and is
not interchangeable with the older warmed multi-build Java profile in §2.6.
All accepted samples performed 79,343 successful membership checks and agreed
on semantic checksum 8da9c6f99f82a731.
| construction API | Rust median | Rust CI95 | Java median | Java CI95 | Java / Rust |
|---|---|---|---|---|---|
arbitrary-order from_terms | 17.246 ms | [17.080, 17.359] ms | 175.798 ms | [171.181, 180.142] ms | 10.193× |
ordered from_sorted_terms | 13.868 ms | [13.818, 13.967] ms | 175.528 ms | [172.427, 179.886] ms | 12.657× |
One and three attempted admissions respectively were rejected and excluded;
each accepted arm has 51 samples and non-overlapping intervals. Because Java's
builder requires ordered input, both causal pairs use the same manifest-proven
ordered corpus. A separate 30-repetition structural matrix exercises the Rust
arbitrary-order path with a seeded shuffle. On the full 79,343-term byte corpus,
unordered construction is 22.065 ms versus 12.302 ms ordered (1.794×); it is
still approximately 7.97× faster than the time-adjacent cold Java median. The
same unit-generic builder also closes the packed-u64 pair at 10.377 ms
unordered and 5.466 ms ordered. Prefix-heavy, suffix-heavy, and mixed-Unicode
cells preserve exact term membership and show that the remaining ordered versus
unordered spread follows input structure rather than a byte-only specialization.
The machine-readable evidence is the
direct query analysis,
the from_terms construction analysis](../../../benchmarks/causal/evidence/2026-08-19/direct-construction-from-terms/analysis.json), the [from_sorted_termsconstruction analysis,
and the structural construction matrix.
The final binary was then measured across the complete, unchanged 45-cell JVM matrix: Standard, OSA/transposition, and merge-and-split; distances one through three; and five query shapes per algorithm. Every cell uses the same 79,343-term dictionary, 1,000-query full-materialization pass, fixed 2 GiB heap, JDK, cpuset, and 2-fork x 10-iteration JMH protocol as the historical baseline. All 45 pairs agree exactly on result count and checksum. Each timed cell passed selected-CPU, SMT-sibling, and shared-LLC admission both before and after measurement.
| final breadth grouping | cells | legacy / Vinary geomean | median | range | Vinary wins |
|---|---|---|---|---|---|
| all shared cells | 45 | 4.882x | 4.923x | [2.910x, 11.828x] | 45 / 45 |
| standard | 15 | 4.223x | 4.298x | [2.910x, 6.457x] | 15 / 15 |
| transposition | 15 | 4.449x | 4.820x | [2.956x, 6.880x] | 15 / 15 |
| merge-and-split | 15 | 6.193x | 5.267x | [4.323x, 11.828x] | 15 / 15 |
This breadth result changes the engineering conclusion from the intermediate matrix in §6.3: managed boundary delivery is no longer a residual performance deficit, merge-and-split is no longer the lagging algorithm, and the original 3x practical target is exceeded on aggregate. The old experiment remains rejected for its measured binary and locked criterion; the post-optimization experiment is accepted for the final binary and evidence.
The authoritative
jvm-parity-full summary
has SHA-256
d067689800c32fd76f2f1572c0481f7e874740627dc83a1535cccc3d8b54c4ba.
Together with §6.4, it proves both sides of the closure: the native engine is
3.308x faster than legacy pure Java at the original anchor, and the complete
Rust-backed JVM product wins every shared end-to-end coordinate.
The final source was rebuilt with -C target-cpu=native and captured with AMD
uProf 5.3.521.0 in CLI-only hotspot mode, pinned to CPU 3. The first post-build
admission sample was rejected because its LLC mean was 11.49%; it was retained
in the ledger and no profile was started. The retry and both pre/post profile
gates passed the 10% selected/SMT/LLC-mean and 20% LLC-peer limits. No Heaptrack
or profiler GUI was opened.
The query profile ran 500 complete standard/d1/hits passes for 7.558 seconds.
Its remaining samples are concentrated in the fused work that the algorithm
must perform:
| optimized query symbol/group | self CPU time | inclusive CPU time |
|---|---|---|
packed expand / queue_children_and_finality | 47.47% | 55.28% |
QueryIterator::next | 36.47% | 95.80% |
| append compact parent path | 2.89% | 2.89% |
| frontier hash-map insertion | 1.45% | 3.33% |
| frontier rehash/reserve | 1.16% | 1.88% |
No arena lock, provider lock, RwLock, Mutex, or graph-projection builder
has a nonzero sample. The earlier transition-processing and arena-locking
bottlenecks have therefore not merely moved to another named synchronization
path: the residual is the fused dictionary/DFA product expansion and iterator
state machine itself. Parent reconstruction and frontier-table maintenance are
already individually below 3%; further work there is micro-optimization, not
another architectural campaign.
The unordered-construction profile ran five warmups and 300 complete builds for 8.560 seconds. Its leading named self-time samples are:
| optimized construction symbol/group | self CPU time | inclusive CPU time |
|---|---|---|
SortedDawgBuilder::minimize_to | 31.35% | 42.06% |
SortedDawgBuilder::insert | 8.99% | 51.06% |
| unstable quicksort partitioning | 6.88% | 27.51% |
| final frozen-node destruction | 5.16% | 7.28% |
malloc | 4.10% | 4.10% |
| merge-registry reserve/rehash | 3.70% | 4.10% |
There is no per-term root publication, arena lock, CAS retry loop, or ArcSwap debt hotspot. The remaining dominant work is exactly unordered construction's required sort followed by right-language minimization. This confirms the earlier causal conclusion: reclamation was dominant only when the old builder created obsolete persistent root paths; it is now a small final-graph cost.
The authoritative
query report
has SHA-256
fc8bc953a3d94b733b4bb4dc6b45628ba1d2ffe09d923335039c6becbe070810;
the
construction report
has SHA-256
c370d90ab6049bc500c5ad52b54b4db7ee38d75d4f061d96346407a888efa16e.
Their exact binary hashes and the immutable host-admission ledger are retained
beside the reports.
The evidence suggests this order:
Every experiment must preserve the four-field result signature, run the structural shapes as well as the anchor, and compare uninstrumented timing with instrumented work in separate executions. An experiment is rejected if it merely moves reclamation off the timed thread, increases unbounded retained memory, specializes to the benchmark dictionary, or changes concurrency/API semantics without an explicit design decision.
The reusable identity gate is
validate_gate.py. It checks
native/resource result equivalence, core-work identity, provider/consumer
descriptor accounting, sorted-constructor path-copy equivalence, and the
batch-size controls.
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 |