Progressive SSZ Benchmark Results After PR #10946
This note summarizes the latest JMH run for progressive SSZ structures versus the corresponding
bounded/non-progressive implementations. The run was captured after merged PR
#10946 was included.
Source data:
benchmark run on a dedicated aws box
Code context:
Consensys/teku#10946
Benchmark context from ProgressiveSszBenchmark:
- Mode: average time
- Unit: microseconds per operation
- Warmup: 3 iterations, 1 second each
- Measurement: 5 iterations, 1 second each
- Forks: 1
- JVM heap:
-Xms2g -Xmx2g
- Byte list and bitlist size: 65,536 elements
- Non-primitive list size: 4,096 elements
- Container field count: 64
- Random access samples: 1,024 indexes
Raw Results
Score and Error are in us/op.
| Benchmark |
Field count |
Representation |
Size |
Score |
Error |
| bitlistCreateView |
N/A |
BOUNDED |
65536 |
8.719 |
0.165 |
| bitlistCreateView |
N/A |
PROGRESSIVE |
65536 |
18.103 |
0.180 |
| bitlistDeserialize |
N/A |
BOUNDED |
65536 |
18.502 |
0.486 |
| bitlistDeserialize |
N/A |
PROGRESSIVE |
65536 |
27.191 |
0.321 |
| bitlistHashTreeRoot |
N/A |
BOUNDED |
65536 |
33.094 |
3.755 |
| bitlistHashTreeRoot |
N/A |
PROGRESSIVE |
65536 |
34.587 |
3.787 |
| bitlistOr |
N/A |
BOUNDED |
65536 |
9.511 |
0.178 |
| bitlistOr |
N/A |
PROGRESSIVE |
65536 |
18.968 |
0.202 |
| bitlistRandomAccess |
N/A |
BOUNDED |
65536 |
10.143 |
0.202 |
| bitlistRandomAccess |
N/A |
PROGRESSIVE |
65536 |
19.193 |
0.189 |
| bitlistSequentialAccess |
N/A |
BOUNDED |
65536 |
54.879 |
4.004 |
| bitlistSequentialAccess |
N/A |
PROGRESSIVE |
65536 |
64.241 |
9.601 |
| bitlistSerialize |
N/A |
BOUNDED |
65536 |
16.087 |
0.497 |
| bitlistSerialize |
N/A |
PROGRESSIVE |
65536 |
34.064 |
0.627 |
| byteListCreateView |
N/A |
BOUNDED |
65536 |
0.025 |
0.001 |
| byteListCreateView |
N/A |
PROGRESSIVE |
65536 |
0.029 |
0.001 |
| byteListDeserialize |
N/A |
BOUNDED |
65536 |
65.578 |
1.499 |
| byteListDeserialize |
N/A |
PROGRESSIVE |
65536 |
72.332 |
0.839 |
| byteListHashTreeRoot |
N/A |
BOUNDED |
65536 |
254.014 |
23.408 |
| byteListHashTreeRoot |
N/A |
PROGRESSIVE |
65536 |
223.742 |
9.459 |
| byteListRandomAccess |
N/A |
BOUNDED |
65536 |
66.612 |
9.717 |
| byteListRandomAccess |
N/A |
PROGRESSIVE |
65536 |
112.381 |
4.736 |
| byteListSequentialAccess |
N/A |
BOUNDED |
65536 |
441.926 |
25.022 |
| byteListSequentialAccess |
N/A |
PROGRESSIVE |
65536 |
692.295 |
4.729 |
| byteListSerialize |
N/A |
BOUNDED |
65536 |
38.444 |
0.563 |
| byteListSerialize |
N/A |
PROGRESSIVE |
65536 |
172.031 |
30.687 |
| containerCreateView |
64 |
BOUNDED |
N/A |
0.055 |
0.001 |
| containerCreateView |
64 |
PROGRESSIVE |
N/A |
0.055 |
0.002 |
| containerDeserialize |
64 |
BOUNDED |
N/A |
2.531 |
0.086 |
| containerDeserialize |
64 |
PROGRESSIVE |
N/A |
2.878 |
0.110 |
| containerHashTreeRoot |
64 |
BOUNDED |
N/A |
9.285 |
0.253 |
| containerHashTreeRoot |
64 |
PROGRESSIVE |
N/A |
9.841 |
0.139 |
| containerRandomAccess |
64 |
BOUNDED |
N/A |
2.677 |
0.047 |
| containerRandomAccess |
64 |
PROGRESSIVE |
N/A |
4.613 |
0.231 |
| containerSequentialAccess |
64 |
BOUNDED |
N/A |
2.641 |
0.010 |
| containerSequentialAccess |
64 |
PROGRESSIVE |
N/A |
4.589 |
0.260 |
| containerSerialize |
64 |
BOUNDED |
N/A |
2.530 |
0.357 |
| containerSerialize |
64 |
PROGRESSIVE |
N/A |
4.401 |
0.482 |
| containerSetAndCommit |
64 |
BOUNDED |
N/A |
0.835 |
0.016 |
| containerSetAndCommit |
64 |
PROGRESSIVE |
N/A |
0.969 |
0.019 |
| containerSetCommitAndHash |
64 |
BOUNDED |
N/A |
1.755 |
0.051 |
| containerSetCommitAndHash |
64 |
PROGRESSIVE |
N/A |
2.526 |
0.051 |
| nonPrimitiveListAppendAndCommit |
N/A |
BOUNDED |
4096 |
2.318 |
0.093 |
| nonPrimitiveListAppendAndCommit |
N/A |
PROGRESSIVE |
4096 |
2.359 |
0.089 |
| nonPrimitiveListCreateView |
N/A |
BOUNDED |
4096 |
0.019 |
0.001 |
| nonPrimitiveListCreateView |
N/A |
PROGRESSIVE |
4096 |
0.030 |
0.001 |
| nonPrimitiveListDeserialize |
N/A |
BOUNDED |
4096 |
1060.831 |
48.634 |
| nonPrimitiveListDeserialize |
N/A |
PROGRESSIVE |
4096 |
1083.291 |
17.930 |
| nonPrimitiveListHashTreeRoot |
N/A |
BOUNDED |
4096 |
2365.807 |
152.269 |
| nonPrimitiveListHashTreeRoot |
N/A |
PROGRESSIVE |
4096 |
2357.008 |
47.610 |
| nonPrimitiveListRandomAccess |
N/A |
BOUNDED |
4096 |
67.679 |
1.115 |
| nonPrimitiveListRandomAccess |
N/A |
PROGRESSIVE |
4096 |
105.893 |
0.716 |
| nonPrimitiveListSequentialAccess |
N/A |
BOUNDED |
4096 |
353.288 |
6.843 |
| nonPrimitiveListSequentialAccess |
N/A |
PROGRESSIVE |
4096 |
478.114 |
6.701 |
| nonPrimitiveListSerialize |
N/A |
BOUNDED |
4096 |
311.817 |
43.602 |
| nonPrimitiveListSerialize |
N/A |
PROGRESSIVE |
4096 |
666.833 |
123.857 |
| nonPrimitiveListSetAndCommit |
N/A |
BOUNDED |
4096 |
1.928 |
0.048 |
| nonPrimitiveListSetAndCommit |
N/A |
PROGRESSIVE |
4096 |
1.893 |
0.064 |
| nonPrimitiveListSetCommitAndHash |
N/A |
BOUNDED |
4096 |
4.027 |
0.078 |
| nonPrimitiveListSetCommitAndHash |
N/A |
PROGRESSIVE |
4096 |
4.833 |
0.278 |
| progressiveBitlistAppendAndCommit |
N/A |
N/A |
65536 |
17.955 |
0.377 |
| progressiveBitlistSetAndCommit |
N/A |
N/A |
65536 |
19.492 |
0.220 |
| progressiveByteListAppendAndCommit |
N/A |
N/A |
65536 |
130.018 |
24.162 |
| progressiveByteListSetAndCommit |
N/A |
N/A |
65536 |
8.558 |
0.406 |
| progressiveByteListSetCommitAndHash |
N/A |
N/A |
65536 |
10.494 |
0.635 |
Progressive Versus Bounded Ratios
Ratio is progressive / bounded. Values below 1.00x mean progressive was faster in this run.
| Benchmark |
Field count |
Size |
Bounded |
Progressive |
Ratio |
Delta |
| bitlistCreateView |
N/A |
65536 |
8.719 |
18.103 |
2.08x |
9.384 |
| bitlistDeserialize |
N/A |
65536 |
18.502 |
27.191 |
1.47x |
8.689 |
| bitlistHashTreeRoot |
N/A |
65536 |
33.094 |
34.587 |
1.05x |
1.493 |
| bitlistOr |
N/A |
65536 |
9.511 |
18.968 |
1.99x |
9.457 |
| bitlistRandomAccess |
N/A |
65536 |
10.143 |
19.193 |
1.89x |
9.050 |
| bitlistSequentialAccess |
N/A |
65536 |
54.879 |
64.241 |
1.17x |
9.362 |
| bitlistSerialize |
N/A |
65536 |
16.087 |
34.064 |
2.12x |
17.977 |
| byteListCreateView |
N/A |
65536 |
0.025 |
0.029 |
1.16x |
0.004 |
| byteListDeserialize |
N/A |
65536 |
65.578 |
72.332 |
1.10x |
6.754 |
| byteListHashTreeRoot |
N/A |
65536 |
254.014 |
223.742 |
0.88x |
-30.272 |
| byteListRandomAccess |
N/A |
65536 |
66.612 |
112.381 |
1.69x |
45.769 |
| byteListSequentialAccess |
N/A |
65536 |
441.926 |
692.295 |
1.57x |
250.369 |
| byteListSerialize |
N/A |
65536 |
38.444 |
172.031 |
4.47x |
133.587 |
| containerCreateView |
64 |
N/A |
0.055 |
0.055 |
1.00x |
0.000 |
| containerDeserialize |
64 |
N/A |
2.531 |
2.878 |
1.14x |
0.347 |
| containerHashTreeRoot |
64 |
N/A |
9.285 |
9.841 |
1.06x |
0.556 |
| containerRandomAccess |
64 |
N/A |
2.677 |
4.613 |
1.72x |
1.936 |
| containerSequentialAccess |
64 |
N/A |
2.641 |
4.589 |
1.74x |
1.948 |
| containerSerialize |
64 |
N/A |
2.530 |
4.401 |
1.74x |
1.871 |
| containerSetAndCommit |
64 |
N/A |
0.835 |
0.969 |
1.16x |
0.134 |
| containerSetCommitAndHash |
64 |
N/A |
1.755 |
2.526 |
1.44x |
0.771 |
| nonPrimitiveListAppendAndCommit |
N/A |
4096 |
2.318 |
2.359 |
1.02x |
0.041 |
| nonPrimitiveListCreateView |
N/A |
4096 |
0.019 |
0.030 |
1.58x |
0.011 |
| nonPrimitiveListDeserialize |
N/A |
4096 |
1060.831 |
1083.291 |
1.02x |
22.460 |
| nonPrimitiveListHashTreeRoot |
N/A |
4096 |
2365.807 |
2357.008 |
1.00x |
-8.799 |
| nonPrimitiveListRandomAccess |
N/A |
4096 |
67.679 |
105.893 |
1.56x |
38.214 |
| nonPrimitiveListSequentialAccess |
N/A |
4096 |
353.288 |
478.114 |
1.35x |
124.826 |
| nonPrimitiveListSerialize |
N/A |
4096 |
311.817 |
666.833 |
2.14x |
355.016 |
| nonPrimitiveListSetAndCommit |
N/A |
4096 |
1.928 |
1.893 |
0.98x |
-0.035 |
| nonPrimitiveListSetCommitAndHash |
N/A |
4096 |
4.027 |
4.833 |
1.20x |
0.806 |
Summary
The run after merged PR #10946 removes the main
progressive byte-list deserialization problem. In the previous run, byteListDeserialize for
progressive byte lists was 1774.097 us/op; in this run it is 72.332 us/op. That is roughly a
24.5x improvement and leaves progressive only 1.10x slower than bounded for this benchmark.
After that fix, progressive byte-list deserialization is no longer the primary concern. The largest
remaining byte-list gap is serialization:
byteListSerialize: progressive 172.031 us/op versus bounded 38.444 us/op, or 4.47x
slower.
byteListSequentialAccess: progressive 692.295 us/op versus bounded 441.926 us/op, or
1.57x slower.
byteListRandomAccess: progressive 112.381 us/op versus bounded 66.612 us/op, or 1.69x
slower.
The mutation results are encouraging for in-place set operations:
progressiveByteListSetAndCommit: 8.558 us/op.
progressiveByteListSetCommitAndHash: 10.494 us/op.
Hashing after a set adds only about 1.936 us/op, which suggests the progressive update path is
getting useful structural sharing from unchanged subtrees.
The unusual mutation number is append:
progressiveByteListAppendAndCommit: 130.018 us/op, with a relatively wide 24.162 us/op
error.
The benchmark appends to a 65,536-byte list. Since bytes pack 32 values per chunk, this append lands
at a fresh chunk boundary. A likely next check is whether the packed update path reads a missing
chunk from the existing progressive tree when chunkIndex == previousTotalChunks; in that case it
could start from LeafNode.EMPTY_LEAF directly.
Considerations
Byte List
The deserialization optimization included in merged PR
#10946 is validated by the data. Progressive
byte-list deserialization is now close to bounded, so further work should move to serialization and
access paths.
The likely serialization issue is repeated progressive tree navigation per chunk. The generic fixed
serialization path asks the progressive tree for each chunk by generalized index. For a byte list,
a faster path could walk the progressive levels/chunks directly and write contiguous leaf data.
The same theme appears in sequential access and getBytes(): SszByteList.getBytes() loops over
every element. For progressive byte lists, a specialized getBytes() or chunk iterator could avoid
per-element access and reduce both sequential-access style workloads and serialization helpers that
need contiguous bytes.
Bitlist
Progressive bitlists are generally slower but not catastrophically so:
- Deserialization is
1.47x.
- Serialization is
2.12x.
- OR and random access are roughly
2x.
- Hash tree root is near parity at
1.05x.
This looks like expected overhead from progressive layout traversal rather than one bad outlier.
Containers
Progressive containers look acceptable in absolute terms. Access and serialization are around
1.7x slower, but the absolute numbers are around 4.4-4.6 us/op for 64 fields. Mutation overhead
is modest:
- Set and commit:
1.16x.
- Set, commit, and hash:
1.44x.
Non-Primitive Lists
Non-primitive list deserialization and hash tree root are effectively at parity:
- Deserialization:
1.02x.
- Hash tree root: approximately
1.00x.
Access remains slower:
- Random access:
1.56x.
- Sequential access:
1.35x.
Serialization is the main non-primitive list gap at 2.14x. Like byte-list serialization, this
points toward progressive traversal overhead during serialization.
Hash Tree Root Results
This run reports real hash-root costs for byte lists, containers, and non-primitive lists, whereas
the earlier run had several near-zero hash-root numbers. That means old versus new hash-root
comparisons should not be treated as apples-to-apples. The latest numbers are more useful for
current decision-making, but the benchmark should continue to be clear about whether it measures
cold hash computation or cached-root access.
Suggested Follow-Up Work
- Investigate progressive byte-list append at the chunk boundary. Check whether appending the first
byte of a new chunk unnecessarily reads the old chunk from the tree.
- Add a progressive byte-list serialization fast path that walks level subtrees/chunks directly
instead of resolving every chunk through a generalized-index lookup.
- Add a progressive byte-list
getBytes() or chunk extraction path to avoid per-byte access when
contiguous bytes are needed.
- Consider analogous direct traversal for non-primitive progressive list serialization if
serialization remains important for real workloads.
- Keep hash-root benchmarks explicit about cold versus cached behavior so future runs remain
comparable.
Progressive SSZ Benchmark Results After PR #10946
This note summarizes the latest JMH run for progressive SSZ structures versus the corresponding
bounded/non-progressive implementations. The run was captured after merged PR
#10946 was included.
Source data:
benchmark run on a dedicated aws box
Code context:
Consensys/teku#10946
Benchmark context from
ProgressiveSszBenchmark:-Xms2g -Xmx2gRaw Results
ScoreandErrorare inus/op.Progressive Versus Bounded Ratios
Ratio is
progressive / bounded. Values below1.00xmean progressive was faster in this run.Summary
The run after merged PR #10946 removes the main
progressive byte-list deserialization problem. In the previous run,
byteListDeserializeforprogressive byte lists was
1774.097 us/op; in this run it is72.332 us/op. That is roughly a24.5ximprovement and leaves progressive only1.10xslower than bounded for this benchmark.After that fix, progressive byte-list deserialization is no longer the primary concern. The largest
remaining byte-list gap is serialization:
byteListSerialize: progressive172.031 us/opversus bounded38.444 us/op, or4.47xslower.
byteListSequentialAccess: progressive692.295 us/opversus bounded441.926 us/op, or1.57xslower.byteListRandomAccess: progressive112.381 us/opversus bounded66.612 us/op, or1.69xslower.
The mutation results are encouraging for in-place set operations:
progressiveByteListSetAndCommit:8.558 us/op.progressiveByteListSetCommitAndHash:10.494 us/op.Hashing after a set adds only about
1.936 us/op, which suggests the progressive update path isgetting useful structural sharing from unchanged subtrees.
The unusual mutation number is append:
progressiveByteListAppendAndCommit:130.018 us/op, with a relatively wide24.162 us/operror.
The benchmark appends to a 65,536-byte list. Since bytes pack 32 values per chunk, this append lands
at a fresh chunk boundary. A likely next check is whether the packed update path reads a missing
chunk from the existing progressive tree when
chunkIndex == previousTotalChunks; in that case itcould start from
LeafNode.EMPTY_LEAFdirectly.Considerations
Byte List
The deserialization optimization included in merged PR
#10946 is validated by the data. Progressive
byte-list deserialization is now close to bounded, so further work should move to serialization and
access paths.
The likely serialization issue is repeated progressive tree navigation per chunk. The generic fixed
serialization path asks the progressive tree for each chunk by generalized index. For a byte list,
a faster path could walk the progressive levels/chunks directly and write contiguous leaf data.
The same theme appears in sequential access and
getBytes():SszByteList.getBytes()loops overevery element. For progressive byte lists, a specialized
getBytes()or chunk iterator could avoidper-element access and reduce both sequential-access style workloads and serialization helpers that
need contiguous bytes.
Bitlist
Progressive bitlists are generally slower but not catastrophically so:
1.47x.2.12x.2x.1.05x.This looks like expected overhead from progressive layout traversal rather than one bad outlier.
Containers
Progressive containers look acceptable in absolute terms. Access and serialization are around
1.7xslower, but the absolute numbers are around4.4-4.6 us/opfor 64 fields. Mutation overheadis modest:
1.16x.1.44x.Non-Primitive Lists
Non-primitive list deserialization and hash tree root are effectively at parity:
1.02x.1.00x.Access remains slower:
1.56x.1.35x.Serialization is the main non-primitive list gap at
2.14x. Like byte-list serialization, thispoints toward progressive traversal overhead during serialization.
Hash Tree Root Results
This run reports real hash-root costs for byte lists, containers, and non-primitive lists, whereas
the earlier run had several near-zero hash-root numbers. That means old versus new hash-root
comparisons should not be treated as apples-to-apples. The latest numbers are more useful for
current decision-making, but the benchmark should continue to be clear about whether it measures
cold hash computation or cached-root access.
Suggested Follow-Up Work
byte of a new chunk unnecessarily reads the old chunk from the tree.
instead of resolving every chunk through a generalized-index lookup.
getBytes()or chunk extraction path to avoid per-byte access whencontiguous bytes are needed.
serialization remains important for real workloads.
comparable.