perf: give branches stable nonce identities - #34
dangerousfood wants to merge 8 commits into
Conversation
Comprehensive A/B gas comparisonBaseline is parent Meaning of the 5,000-order resultThis measures one additional rest into a side that already contains 5,000 orders. Setup is outside the gas meter. It does not mean that constructing the entire 5,000-order book is 29.7% more expensive. The old packed branch word contained a descendant order path, so Patricia split decisions could read a routing key directly from the branch. The new low 32 bits are an independent branch identity. The current implementation calls
A controlled one-side insertion sweep shows the scaling directly:
The absolute 5,000-order overhead is consistently about 51k; the percentage differs from the checked-in benchmark because each harness has different fixed costs. Intended optimization: off-spine quantity reduction
This is the intended win: every surviving ancestor keeps its nonce-addressed child slot, so the unwind rewrites the parent pointer rather than relocating both child pointers. Exact-key cancellation tradeoff
Cancellation receives no child-slot-move benefit when collapsing/promoting nodes, but it still pays for descendant-key routing. This is the largest regression and should not be hidden. Complete checked-in snapshot coverageThe snapshot contains 80 shared cases plus 9 new cases. The categories below account for every entry:
For the 19 ordinary base-profile paths, every individual delta is between +0.07% and +0.28%. Large-book best-leaf partial fills are only +0.17% because the existing dirty-right-spine optimization already avoided cascading rewrites there. Integrator profile: three match paths are +6 gas, empty-book rest is +204 gas (+0.13%), and repeated-fee-token routing improves by 1,988 gas (-1.62%). The earlier -40.5% / -94.4% figures were non-isolated ConclusionThis PR is a strong, depth-amplifying optimization for off-spine quantity mutation, but it is not a universal gas reduction. The current descendant-key recovery creates meaningful insertion and exact-cancellation regressions. A follow-up can likely remove most of those reads by using the stored representative price for price-divergent branches and descending only when the Patricia split is in the nonce half (same-price routing). |
|
Implemented the follow-up routing redesign in The branch nonce now carries both stable identity and the cached Patricia split depth: This eliminates the previous gas cliffs:
These are canonical |
Final comprehensive gas diff (
|
| Suite | Cases | Cheaper | Higher | Aggregate | Median | Worst regression |
|---|---|---|---|---|---|---|
| Base paths (no hook / no fee) | 25 | 4 | 21 | -2.507% | +0.163% | +0.319% |
| Hook-enabled paths | 25 | 4 | 21 | -2.241% | +0.117% | +0.335% |
| Protocol-fee paths | 25 | 4 | 21 | -2.483% | +0.162% | +0.318% |
| Integrator fee paths | 5 | 1 | 4 | -0.319% | +0.006% | +0.013% |
| All shared cases | 80 | 13 | 67 | -2.297% | +0.129% | +0.335% |
The result is not literally cheaper in every row. Most common-path movements are 25–240 gas (≤0.335%), while deep mutation paths fall by tens of thousands of gas and aggregate gas falls 2.30%.
Base paths (no hook / no fee) — all shared cases
| Path | Parent | Final | Gas Δ | % Δ |
|---|---|---|---|---|
CancelAskSkipsPathologicalBidTree |
39,664 | 39,778 | +114 | +0.287% |
CancelFilledBidClaim |
36,750 | 36,852 | +102 | +0.278% |
CancelFullDepthBidCombRightmost |
423,078 | 376,773 | -46,305 | -10.945% |
CancelMaxValidDepthAskCombRightmost |
424,751 | 376,482 | -48,269 | -11.364% |
CancelPartialBid |
48,804 | 48,918 | +114 | +0.234% |
CancelUnfilledAsk |
39,881 | 39,995 | +114 | +0.286% |
CancelUnfilledBid |
39,717 | 39,831 | +114 | +0.287% |
FillAskConsumesDirtySamePriceBidSubtree |
87,242 | 87,411 | +169 | +0.194% |
FillAskConsumesFullDepthBidComb |
327,303 | 328,313 | +1,010 | +0.309% |
FillAskFullyMatchesSingleBid |
77,593 | 77,713 | +120 | +0.155% |
FillAskPartiallyMatchesSingleBid |
97,288 | 97,408 | +120 | +0.123% |
FillBidConsumesAskAndRestsRemainder |
130,241 | 130,314 | +73 | +0.056% |
FillBidConsumesDirtySamePriceAskSubtree |
87,376 | 87,567 | +191 | +0.219% |
FillBidConsumesMaxValidDepthAskComb |
323,500 | 324,532 | +1,032 | +0.319% |
FillBidConsumesSamePriceAskSubtree |
77,368 | 77,422 | +54 | +0.070% |
FillBidFullyMatchesSingleAsk |
77,268 | 77,388 | +120 | +0.155% |
FillBidPartiallyMatchesSingleAsk |
97,182 | 97,302 | +120 | +0.123% |
FillRestAskEmptyBook |
150,668 | 150,806 | +138 | +0.092% |
FillRestBidEmptyBook |
152,432 | 152,565 | +133 | +0.087% |
LargeRandomBookAskMatchesOneBid |
121,497 | 121,695 | +198 | +0.163% |
LargeRandomBookBidMatchesOneAsk |
136,708 | 136,942 | +234 | +0.171% |
LargeRandomBookPartialFillsAsk |
137,846 | 138,086 | +240 | +0.174% |
LargeRandomBookPartialFillsBid |
122,621 | 122,825 | +204 | +0.166% |
LargeRandomBookRestsAsk |
163,742 | 163,609 | -133 | -0.081% |
LargeRandomBookRestsBid |
173,067 | 172,983 | -84 | -0.049% |
Hook-enabled paths — all shared cases
| Path | Parent | Final | Gas Δ | % Δ |
|---|---|---|---|---|
CancelAskSkipsPathologicalBidTree |
43,705 | 43,780 | +75 | +0.172% |
CancelFilledBidClaim |
37,497 | 37,563 | +66 | +0.176% |
CancelFullDepthBidCombRightmost |
446,433 | 400,481 | -45,952 | -10.293% |
CancelMaxValidDepthAskCombRightmost |
447,267 | 399,351 | -47,916 | -10.713% |
CancelPartialBid |
53,592 | 53,669 | +77 | +0.144% |
CancelUnfilledAsk |
43,922 | 43,997 | +75 | +0.171% |
CancelUnfilledBid |
44,505 | 44,582 | +77 | +0.173% |
FillAskConsumesDirtySamePriceBidSubtree |
95,252 | 95,373 | +121 | +0.127% |
FillAskConsumesFullDepthBidComb |
414,778 | 416,130 | +1,352 | +0.326% |
FillAskFullyMatchesSingleBid |
85,769 | 85,841 | +72 | +0.084% |
FillAskPartiallyMatchesSingleBid |
105,472 | 105,544 | +72 | +0.068% |
FillBidConsumesAskAndRestsRemainder |
139,218 | 139,243 | +25 | +0.018% |
FillBidConsumesDirtySamePriceAskSubtree |
94,243 | 94,386 | +143 | +0.152% |
FillBidConsumesMaxValidDepthAskComb |
410,116 | 411,490 | +1,374 | +0.335% |
FillBidConsumesSamePriceAskSubtree |
93,348 | 93,384 | +36 | +0.039% |
FillBidFullyMatchesSingleAsk |
84,195 | 84,267 | +72 | +0.086% |
FillBidPartiallyMatchesSingleAsk |
104,117 | 104,189 | +72 | +0.069% |
FillRestAskEmptyBook |
140,357 | 140,447 | +90 | +0.064% |
FillRestBidEmptyBook |
142,118 | 142,203 | +85 | +0.060% |
LargeRandomBookAskMatchesOneBid |
138,973 | 139,140 | +167 | +0.120% |
LargeRandomBookBidMatchesOneAsk |
154,786 | 154,995 | +209 | +0.135% |
LargeRandomBookPartialFillsAsk |
146,871 | 147,063 | +192 | +0.131% |
LargeRandomBookPartialFillsBid |
133,256 | 133,412 | +156 | +0.117% |
LargeRandomBookRestsAsk |
193,723 | 193,607 | -116 | -0.060% |
LargeRandomBookRestsBid |
198,650 | 198,565 | -85 | -0.043% |
Protocol-fee paths — all shared cases
| Path | Parent | Final | Gas Δ | % Δ |
|---|---|---|---|---|
CancelAskSkipsPathologicalBidTree |
39,664 | 39,778 | +114 | +0.287% |
CancelFilledBidClaim |
36,750 | 36,852 | +102 | +0.278% |
CancelFullDepthBidCombRightmost |
423,078 | 376,773 | -46,305 | -10.945% |
CancelMaxValidDepthAskCombRightmost |
424,771 | 376,502 | -48,269 | -11.364% |
CancelPartialBid |
48,804 | 48,918 | +114 | +0.234% |
CancelUnfilledAsk |
39,881 | 39,995 | +114 | +0.286% |
CancelUnfilledBid |
39,717 | 39,831 | +114 | +0.287% |
FillAskConsumesDirtySamePriceBidSubtree |
87,846 | 88,015 | +169 | +0.192% |
FillAskConsumesFullDepthBidComb |
352,999 | 354,009 | +1,010 | +0.286% |
FillAskFullyMatchesSingleBid |
78,197 | 78,317 | +120 | +0.153% |
FillAskPartiallyMatchesSingleBid |
97,892 | 98,012 | +120 | +0.123% |
FillBidConsumesAskAndRestsRemainder |
130,838 | 130,911 | +73 | +0.056% |
FillBidConsumesDirtySamePriceAskSubtree |
87,970 | 88,161 | +191 | +0.217% |
FillBidConsumesMaxValidDepthAskComb |
324,094 | 325,126 | +1,032 | +0.318% |
FillBidConsumesSamePriceAskSubtree |
77,962 | 78,016 | +54 | +0.069% |
FillBidFullyMatchesSingleAsk |
77,862 | 77,982 | +120 | +0.154% |
FillBidPartiallyMatchesSingleAsk |
97,776 | 97,896 | +120 | +0.123% |
FillRestAskEmptyBook |
150,894 | 151,032 | +138 | +0.091% |
FillRestBidEmptyBook |
152,651 | 152,784 | +133 | +0.087% |
LargeRandomBookAskMatchesOneBid |
122,101 | 122,299 | +198 | +0.162% |
LargeRandomBookBidMatchesOneAsk |
137,302 | 137,536 | +234 | +0.170% |
LargeRandomBookPartialFillsAsk |
138,440 | 138,680 | +240 | +0.173% |
LargeRandomBookPartialFillsBid |
123,225 | 123,429 | +204 | +0.166% |
LargeRandomBookRestsAsk |
163,968 | 163,835 | -133 | -0.081% |
LargeRandomBookRestsBid |
173,286 | 173,202 | -84 | -0.048% |
Integrator fee paths — all shared cases
| Path | Parent | Final | Gas Δ | % Δ |
|---|---|---|---|---|
IntegratorAndProtocolFeeFill |
129,374 | 129,380 | +6 | +0.005% |
IntegratorFillAskFullyMatchesSingleBid |
103,768 | 103,774 | +6 | +0.006% |
IntegratorFillBidFullyMatchesSingleAsk |
103,888 | 103,894 | +6 | +0.006% |
IntegratorFillRestBidEmptyBook |
151,809 | 151,828 | +19 | +0.013% |
IntegratorRouteNetsRepeatedFeeToken |
122,504 | 120,516 | -1,988 | -1.623% |
Targeted cases added on this branch
These three benchmarks did not exist in the parent snapshot file, so they were run separately against the parent implementation with the same isolated command and fixtures.
| Base path | Parent | Final | Delta |
|---|---|---|---|
| Ask partially fills off-spine bid branch | 159,849 | 118,438 | -25.91% |
| Bid partially fills off-spine ask branch | 159,696 | 118,286 | -25.93% |
| 64-level pathological off-spine partial fill | 3,255,481 | 644,069 | -80.22% |
First attempt → final smoothing
| Cliff in the first stable-identity version | First attempt | Final | Improvement |
|---|---|---|---|
| Rest ask into 5,000-order side | 212,228 | 163,609 | -22.91% |
| Rest bid into 5,000-order side | 224,480 | 172,983 | -22.94% |
| Cancel 64-deep bid comb | 584,264 | 376,773 | -35.51% |
| Cancel max-depth ask comb | 584,741 | 376,482 | -35.62% |
| Pathological off-spine partial fill | 713,602 | 644,069 | -9.74% |
Final verification: 89/89 isolated gas cases, 401/401 behavioral tests, 38/38 invariants at 256×64 with zero handler reverts, and 241/241 coverage tests with adjusted 100% lines/statements/branches/functions.
|
Updated the implementation to preserve the full 32-bit branch identity field—no Routing depth now comes from child ticks for mixed-price branches and from redundant stored-left tick bits for same-price branches. The externally visible child is decoded back to its exact node. The two-slot Added a boundary test with branch identity |
Summary
uint32branch identity in the packed branch summary stored in its parenttree[bytes32(branchIdentity)], so quantity/correction rewrites never relocate child slotsBranchstorage while retaining cheap routing:nonceAndFlagsword through match/rest/cancel routing instead of reloading itThis removes the cascading child-slot moves caused by quantity/correction changes without reducing the identity field to 26 bits. Both order nonces and branch identities use the complete 32-bit node-identity field. A boundary test allocates a branch identity with bit 31 set and verifies it is preserved exactly.
Existing branches retain their identity when their packed summary changes; only a new topology node consumes a branch identity. The ascending branch and descending order fronts are shared across both sides of one book and are checked before every rest, so a branch identity cannot alias an order nonce.
Why not a separate flat
bookId + noncestorage scheme?Solidity's nested mapping already derives the effective branch location from those two identities: the book base comes from
bookId, andtree[branchIdentity]hashes the identity with that book's tree slot. Passing an explicitbookIdinto unstructured storage would replace that hash with a longer preimage; it would not remove the coldSLOAD/SSTORE. The material saving comes from keeping the branch identity stable so child slots never move.Incremental gas result for the follow-up optimizations
Canonical isolated snapshots compare the prior full-width implementation (
1ce25f8) with this commit (247e715). The values below are sums of separate benchmark transactions used only as an aggregate regression signal; 4.44M is not the gas cost of one fill.The median case improves 0.171%. The largest incremental improvement is the pathological partial-fill comb at -3.053%. The largest incremental regression is only +0.079% and is a hook-enabled unfilled cancellation; opcode tracing confirms that path has the same four
SLOADs and twoSSTOREs, so the small change is control-flow/stack overhead rather than an added storage operation.Representative base paths
Measured storage-op changes
Debugger opcode dumps isolate the final engine call in each benchmark:
Combined comparison with the PR parent
Across the 80 runtime-snapshot cases shared with the parent, aggregate gas falls 0.83%. The median change is +0.119%; the largest remaining regression is +2.11% on deep full-subtree consumption. The stable-identity architecture therefore removes the severe partial-update and cancellation cliffs, while the follow-up changes recover gas broadly across the common match/rest paths.
Verification
Local verification on
247e715:forge fmt --checkforge lint(warnings only)forge build --sizes src/DeepstateV1.sol(23,354-byte runtime; 1,222-byte margin)CI reruns the full sharded invariant, coverage, Halmos, SMT, static-analysis, build-size, and gas gates for the pushed commit.