Repository navigation
perf: batch merkleize levels and speed up SHA-256 pair hashing - #40
Draft
MegaRedHand wants to merge 1 commit into
Draft
MegaRedHand wants to merge 1 commit into
MegaRedHand wants to merge 1 commit into
Conversation
- `Sha256Hasher::hash_pairs` hashes many 64-byte pairs per call. It has a default implementation, so existing hashers keep working unchanged. `merkleize` hands each tree level to it in one batch, and `hash_nodes` routes single pairs through it. - `merkleize` hashes level 0 straight from the input chunks instead of copying them first. - `Sha2Hasher` moves to `sha2` 0.11, which selects ARMv8 SHA2 instructions at runtime (0.10 only used them with its `asm` feature), and hashes pairs by calling the compression function directly on the message plus a constant padding block. - New opt-in `hashtree` feature: pairs are hashed by the hashtree assembly library (SHA-NI, AVX-512/AVX2 multi-buffer, ARMv8 SHA2, NEON). Requires a C toolchain; CI tests it as a separate matrix entry.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Makes
hash_tree_rootabout 1.3x faster onBeaconStateon x86_64 with the default backend, and about 1.7x faster with a new opt-inhashtreefeature. Roots are unchanged: all consensus spec tests pass with both backends.What changes
Sha256Hasher::hash_pairs(&self, pairs, out)hashes many 64-byte pairs per call.merkleizehands it each tree level in one batch, andhash_nodesroutes single pairs through it. It has a default implementation that callshashonce per pair, so existingSha256Hasherimplementations keep working.merkleizehashes the first level straight from the input chunks instead of copying them first.sha20.10 → 0.11:Sha2Hasherhashes a pair by callingsha2's compression function directly on the 64-byte message plus a constant padding block, skipping theDigestbuffering.sha20.11 also detects the ARMv8 SHA-256 instructions at runtime; 0.10 only used them with itsasmfeature. x86 already got SHA-NI at runtime with 0.10.hashtreefeature (libssz-merkle/hashtree, off by default): batches go to the hashtree assembly library throughhashtree-rs0.2.6. It is specialized for 64-byte inputs and hashes several pairs at once (SHA-NI, AVX-512, AVX2, ARMv8 SHA2, NEON). It needs a C toolchain andmakeat build time. CI tests it as a new entry in the test matrix.docs/technical-design.mddescribes the batched levels and thehashtreeoption.No public API is removed or renamed.
Tests
crates/ssz-merkle/src/sha256.rs:sha2::Sha256::digest;hash_pairsis checked against it for every batch size from 0 to 33 pairs, which covers every SIMD remainder inhashtree;--features libssz-merkle/hashtree.Benchmarks (x86_64)
AMD Ryzen 9 9950X3D (Zen 5, SHA-NI), Linux 6.12, rustc 1.95.0,
benchprofile, criterion medians. Every bench was pinned to one core withtaskset -c 4.mainis 958a90f.The full suite ran once per variant, then interleaved rounds repeated a subset. Those agreed within about 2% on every row above 100 ns. The Lighthouse and ssz_rs rows don't use libssz's hashing and act as a control.
Brackets: speedup vs
main(higher is faster).mainhashtreemerkle/hash_nodes/singlemerkle/mix_in_length/singlehash_tree_root/consensus_containers/checkpointhash_tree_root/containers/validatorhash_tree_root/list_validator/10000hash_tree_root/beacon_state/16384hash_tree_root/beacon_state/300000diff/htr/beacon_state/libssz/1000000hash_tree_root/list_u64/100000merkle/merkleize/4096merkle/merkleize_with_limit/chunks/65536_of_262144diff/htr/beacon_state/lighthouse/1000000diff/htr/beacon_state/ssz_rs/1000000merkle/pack/32768main: 1.05–1.54x faster on every benchmark that hashes, except a lonehash_nodes(below). It is about 1.3x faster onBeaconStateand validators and up to 1.54x on largeu64lists. Small objects that only hash a few pairs gain least (1.05x onunions/0).hashtree: 1.16–2.04x faster, and about 1.7x onBeaconState.merkle/hash_nodes/singleis the one regression: 53 → 64 ns, and I haven't found why. It reproduced in three passes. A standalone probe on the same machine doesn't show it: the new pair hash takes 40 ns for independent hashes and 63 ns for chained ones, against 52–69 ns and 66–70 ns forsha20.10'sDigest. The other one-hash benchmarks got faster (mix_in_length/single67 → 64 ns,checkpoint77 → 65 ns).sha20.11 bump should matter most there, because it enables the ARMv8 SHA-256 instructions. On an Apple M3, enabling them onmainthroughsha20.10'sasmfeature madehash_tree_rootabout 5x faster (see feat!: use Keccak-256 instead of SHA-256 for hash_tree_root (experiment) #39).The same runs also compared Keccak-256 (#39).
All 91 benchmarks (x86_64)
merkle/merkleize/1doesn't hash, but it runs about 4x faster with this change.mainhashtreemainhashtreevsmaindiff/htr/beacon_state/libssz/100000diff/htr/beacon_state/libssz/1000000diff/htr/beacon_state/libssz/16384diff/htr/beacon_state/libssz/300000diff/htr/beacon_state/lighthouse/100000diff/htr/beacon_state/lighthouse/1000000diff/htr/beacon_state/lighthouse/16384diff/htr/beacon_state/lighthouse/300000diff/htr/beacon_state/ssz_rs/100000diff/htr/beacon_state/ssz_rs/1000000diff/htr/beacon_state/ssz_rs/16384diff/htr/beacon_state/ssz_rs/300000diff/htr/consensus_containers/libssz/attestation_datadiff/htr/consensus_containers/libssz/checkpointdiff/htr/consensus_containers/libssz/eth1_datadiff/htr/consensus_containers/libssz/forkdiff/htr/libssz/booldiff/htr/libssz/bytes32diff/htr/libssz/u64diff/htr/lighthouse/booldiff/htr/lighthouse/bytes32diff/htr/lighthouse/u64diff/htr/ssz_rs/booldiff/htr/ssz_rs/bytes32diff/htr/ssz_rs/u64hash_tree_root/beacon_state/100000hash_tree_root/beacon_state/16384hash_tree_root/beacon_state/300000hash_tree_root/bitfields/bitlist/2048hash_tree_root/bitfields/bitlist/4096hash_tree_root/bitfields/bitlist/64hash_tree_root/bitfields/bitvector/4096hash_tree_root/bitfields/bitvector/512hash_tree_root/bitfields/bitvector/64hash_tree_root/consensus_containers/attestation_datahash_tree_root/consensus_containers/checkpointhash_tree_root/consensus_containers/eth1_datahash_tree_root/consensus_containers/forkhash_tree_root/consensus_containers/pending_attestationhash_tree_root/containers/beacon_block_headerhash_tree_root/containers/validatorhash_tree_root/list_u64/100hash_tree_root/list_u64/1000hash_tree_root/list_u64/100000hash_tree_root/list_validator/100hash_tree_root/list_validator/1000hash_tree_root/list_validator/10000hash_tree_root/nested_container/10hash_tree_root/nested_container/100hash_tree_root/nested_container/1000hash_tree_root/primitives/boolhash_tree_root/primitives/bytes32hash_tree_root/primitives/u128hash_tree_root/primitives/u16hash_tree_root/primitives/u32hash_tree_root/primitives/u64hash_tree_root/primitives/u8hash_tree_root/unions/0hash_tree_root/unions/1hash_tree_root/unions/2hash_tree_root/unions/3hash_tree_root/variable_container/100hash_tree_root/variable_container/1000hash_tree_root/variable_container/10000hash_tree_root/vec_u64/100hash_tree_root/vec_u64/1000hash_tree_root/vec_u64/100000hash_tree_root/vector_bytes32/100hash_tree_root/vector_bytes32/1000merkle/hash_nodes/singlemerkle/merkleize/1merkle/merkleize/1024merkle/merkleize/16merkle/merkleize/256merkle/merkleize/4merkle/merkleize/4096merkle/merkleize/64merkle/merkleize_with_limit/chunks/1024_of_1048576merkle/merkleize_with_limit/chunks/256_of_4096merkle/merkleize_with_limit/chunks/64_of_1024merkle/merkleize_with_limit/chunks/65536_of_262144merkle/merkleize_with_limit/chunks/65537_of_262144merkle/mix_in_length/singlemerkle/pack/1024merkle/pack/256merkle/pack/32merkle/pack/32768merkle/pack_bits/2048merkle/pack_bits/512merkle/pack_bits/64merkle/pack_bits/8192