perf: eliminate per-element Vec<u8> allocation in Merkle tree hashing#503
Conversation
Add WriteBytes trait for zero-allocation byte serialization and use it in the prover's Merkle commitment hot paths. Problem: as_bytes() returns Vec<u8> (heap allocation) for every field element. For CPU table commitment (2^20 rows × 74 cols), that's ~77M allocations of 8-byte Vecs. Benchmarks show this as a 1.3-1.7x slowdown. Solution: - WriteBytes trait: write_bytes_be(&self, buf: &mut [u8]) writes to caller-provided buffer, zero heap allocations per element. - Implemented for Goldilocks (8 bytes) and cubic extension (24 bytes). - commit_columns_bit_reversed: single buffer per row, write all columns directly, hash once. Eliminates both the row Vec and per-element Vec. - commit_composition_polynomial: same approach for composition pairs. The AsBytes trait and FieldElementVectorBackend are unchanged — only the prover hot paths are optimized. Verifier and generic code still use as_bytes() for backward compatibility. Benchmarked in lambdaworks (~/dev/lambdaworks bench/merkle-comparison): - 16 cols: 1.3-1.5x speedup, beats Plonky3 by 1.3-1.4x - 64 cols: 1.6-1.8x speedup, beats Plonky3 by 1.5-1.6x
|
/bench 3 |
Benchmark — fib_iterative_8M (median of 10)Table parallelism: 32 (auto = cores / 3)
Commit: 65c73af · Baseline: built from main · Runner: self-hosted bench |
Codex Code Review
Assumptions:
|
Review: perf/merkle-zero-alloc-hashingHash correctness confirmed. The old path called Two issues found:
|
Codex Code ReviewFindings
Security review note
|
PR Review: Zero-Allocation Merkle Leaf HashingThe optimization direction (eliminating per-element heap allocations in the Merkle leaf hash path) is correct and meaningful. One high-severity issue and a few medium/low issues below. High: Prover–Verifier Hash Function CouplingThe most critical issue: the prover now hardcodes Inline comments on specific lines below. |
MauroToscano
left a comment
There was a problem hiding this comment.
Changing a bit the design to keep the optimization but be able to swap hashes smoothly, and use algebraics in the future
|
/bench 10 |
…ytes (#506) Add hash_bytes() inherent method to FieldElementVectorBackend that surfaces the generic digest parameter D. The prover now calls BatchedMerkleTreeBackend::<E>::hash_bytes() instead of hardcoding sha3::Keccak256, so changing the config.rs type alias automatically propagates the hash function to the commit hot paths. Also adds missing debug_assert for power-of-two in commit_columns_bit_reversed for consistency with commit_composition_polynomial.
Consolidate serialization traits by adding BYTE_LEN and write_bytes_be to ByteConversion instead of having a separate WriteBytes trait. Goldilocks and cubic extension override write_bytes_be with zero-alloc implementations; other types use the default which delegates to to_bytes_be.
|
/bench 10 |
Add WriteBytes trait for zero-allocation byte serialization and use it in the prover's Merkle commitment hot paths.
Problem: as_bytes() returns Vec (heap allocation) for every field element. For CPU table commitment (2^20 rows × 74 cols), that's ~77M allocations of 8-byte Vecs. Benchmarks show this as a 1.3-1.7x slowdown.
Solution:
The AsBytes trait and FieldElementVectorBackend are unchanged — only the prover hot paths are optimized. Verifier and generic code still use as_bytes() for backward compatibility.
Benchmarked in lambdaworks (~/dev/lambdaworks bench/merkle-comparison):