Skip to content

further ssz optimizations #10947

Description

@tbenr

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

  1. 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.
  2. Add a progressive byte-list serialization fast path that walks level subtrees/chunks directly
    instead of resolving every chunk through a generalized-index lookup.
  3. Add a progressive byte-list getBytes() or chunk extraction path to avoid per-byte access when
    contiguous bytes are needed.
  4. Consider analogous direct traversal for non-primitive progressive list serialization if
    serialization remains important for real workloads.
  5. Keep hash-root benchmarks explicit about cold versus cached behavior so future runs remain
    comparable.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions