Commit 1106f14
committed
perf(utxo): shrink the record payload 21.7% and make lookups faster than v4
The attribution in the previous commit put the UTXO set at 77.4% of process RSS
and the tip projection at 83% of the 16 GiB G14 budget on the UTXO path alone,
before txindex and blockfilterindex. This is the encoding work that margin
justified.
v5 keeps the record header and replaces the per-output layout:
txid(32) || output_count(4) || legacy_inline_len(1) || widths(1)
|| vout_dir : one fixed-width little-endian entry per output
|| len_dir : one fixed-width payload length per output
|| payloads : varint(amount) [|| raw amount] || varint(height<<1|coinbase) || script
Three transforms do the shrinking, all per-output with no cross-output invariant
to violate: Core's `CTxOutCompressor` amount transform, `height` and `coinbase`
packed into one varint, and directory widths that are the narrowest the record
needs. The script length is not stored — the script is whatever remains of its
payload, so the length directory pays for itself.
Measured: **11.75 bytes per output, 21.7% of the payload**, which is 14.8% of
process RSS and about 1.97 GiB at tip. Hoisting `height` into the record header
would save 3 bytes more and is deliberately not done: it needs "every output of
a record shares one height", and BIP30's duplicate coinbase txids are exactly
where that might not hold.
The directories are the load-bearing part, and they exist because the first
draft was wrong. That draft was a flat varint frame per output. It hit the size
target and lost badly on speed:
operation v4 flat v5 directory v5
get_miss (shard lookup) 705 ns 3.42 µs 300 ns
get_last 728 ns 3.43 µs 617 ns
get_middle 384 ns 1.67 µs 342 ns
spend_fanout_64 18.5 µs 39.9 µs 21.3 µs
spend_fanout_64_noop 86.7 µs 115.2 µs 77.1 µs
Two mistakes produced that, and neither was visible until the benchmark was
reshaped around the operation that actually dominates:
1. The benchmark timed whole-record encode/decode. The hot read is
`find_output(vout)` — every spent input resolves through `Shard::get`,
`get_entry` or `get_meta`, and all three land there. Whole-record decode is
the snapshot and rescan path, which is rare by comparison.
2. v4 gets lazy field skipping for free and a flat varint layout cannot. Every
v4 field sits at a constant offset, so when only `vout` is read the optimizer
deletes the loads for the rest. In a flat layout each varint's length locates
the next field, so the reads are a serial dependency chain, and finding
output `i` walks the bytes of outputs `0..i`, scripts included.
The directories remove exactly that: a lookup scans one dense fixed-width array
and sums a second, touching ~2 bytes per output instead of ~35. Nine of the
twelve `utxo_commit` lookup arms now beat v4; the three that do not are the
`_first` cases, 10 ns apart in absolute terms.
Encoding is 1.6-2.4x slower, because the directory widths are a property of the
whole record so nothing can be written until every payload length is known. At
block scale that is commit p95 +3% (`existing`), +8% (`uniform`) and +21%
(`concentrated`). The G14 budget is 50 ms and the worst case measured is 2.57 ms.
Checked by:
- `tests/record_codec_equivalence.rs`, 7 tests. v4 is retained as the oracle;
equality is per field over every decoded `OneUtxoOut`, in order, since
comparing encoded bytes is meaningless when the layouts differ by design.
Size is asserted as a property, not a spot check.
- `non_canonical_v5_spellings_are_rejected` covers every second spelling the two
layouts introduce: a non-minimal varint, the amount escape used for a value
the compact form already covers, and a directory wider than the record needs.
`UtxoRecord` compares by bytes, so two spellings of one record is a
correctness bug.
- `find_output_decompresses_at_most_the_amount_it_returns` asserts the work
rather than the time: one amount decompression for a hit, none for a miss,
none for `max_vout`. A wall-clock assertion in a test suite is a flake
generator; counting the expensive operation is the same claim made
deterministically.
The `utxo_commit` arms cannot be paired in one run since only one codec is
compiled in, so the v4 comparison was taken A-B-A across a stash, with the two
v5 runs agreeing to 0.1-2.9%.1 parent 0b07e13 commit 1106f14
7 files changed
Lines changed: 1852 additions & 63 deletions
File tree
- crates/utxo
- benches
- src
- tests
- docs/benchmarks
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
57 | 57 | | |
58 | 58 | | |
59 | 59 | | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
0 commit comments