perf: Avoid heap allocations in state, input buffers and byte hash path - #61
vadorovsky wants to merge 2 commits into
Conversation
42762d0 to
e039a09
Compare
The Poseidon state holds at most `MAX_X5_LEN` (13) field elements and the number of inputs is at most 12, so neither buffer needs dynamic allocation. `Poseidon.state` is now an `ArrayVec<F, MAX_X5_LEN>`. The per-round `collect()` in `apply_mds` no longer mallocs and frees a fresh `Vec` (one allocation pair per round, 64-74 per hash) - the replacement state is built on the stack and moved into place. `hash_bytes_be`/`hash_bytes_le` deserialize inputs into a single stack-resident `ArrayVec<F, MAX_INPUTS>` instead of building two intermediate `Vec`s (validation results and converted elements). This adds the `arrayvec` dependency and a public `MAX_INPUTS` constant. Two incidental refactors: the input-length check is extracted into `validate_inputs_length`, and `validate_bytes_length` now returns `Result<(), PoseidonError>` instead of `Result<&[u8], PoseidonError>` - the identity return was only used by the old map/collect pattern. Note that this last part is a public API change.
The byte-input path still performed small heap allocations per call.
`bytes_to_prime_field_element_be/le` built a `num_bigint::BigUint`
(a heap `Vec<u32>`) for every input. The value is now assembled directly
in `F::BigInt` (a fixed-size stack array of limbs) from the bytes, most
significant byte first, and checked with `F::from_bigint`, which
performs the modulus check in ark-ff 0.5.
`hash_bytes_be/le` converted the result via `to_bytes_be/le`, which
return a `Vec<u8>`. A new `IntoHashBytes` extension trait for
`PrimeField` serializes the result into the fixed-size output array
directly from its limbs instead.
This removes the `num-bigint` dependency. Edge-case behavior is
preserved: short inputs and oversized inputs with leading zero bytes
are still accepted, `BytesToBigInt` is still returned for values that
do not fit in `F::BigInt`, and `InputLargerThanModulus` for values
>= the modulus.
Also drops a needless borrow in `hash` flagged by clippy.
Benchmarked on AMD EPYC 7H12 64-core with the following results.
Current `main`:
```
poseidon_bn254_x5_1 time: [24.091 µs 24.130 µs 24.180 µs]
poseidon_bn254_x5_2 time: [34.334 µs 34.372 µs 34.420 µs]
poseidon_bn254_x5_3 time: [46.463 µs 46.499 µs 46.544 µs]
poseidon_bn254_x5_4 time: [65.584 µs 65.697 µs 65.855 µs]
poseidon_bn254_x5_5 time: [84.879 µs 85.005 µs 85.172 µs]
poseidon_bn254_x5_6 time: [113.84 µs 114.37 µs 114.97 µs]
poseidon_bn254_x5_7 time: [144.51 µs 144.71 µs 144.95 µs]
poseidon_bn254_x5_8 time: [176.00 µs 176.13 µs 176.28 µs]
poseidon_bn254_x5_9 time: [205.66 µs 205.80 µs 205.95 µs]
poseidon_bn254_x5_10 time: [272.40 µs 273.08 µs 273.98 µs]
poseidon_bn254_x5_11 time: [298.14 µs 298.41 µs 298.69 µs]
poseidon_bn254_x5_12 time: [378.33 µs 378.96 µs 379.70 µs]
```
This change + arrayvec migration:
```
poseidon_bn254_x5_1 time: [22.893 µs 22.909 µs 22.925 µs]
change: [-5.0821% -4.9214% -4.7493%] (p = 0.00 < 0.05)
Performance has improved.
Found 7 outliers among 100 measurements (7.00%)
4 (4.00%) high mild
3 (3.00%) high severe
poseidon_bn254_x5_2 time: [33.052 µs 33.081 µs 33.113 µs]
change: [-3.9722% -3.8519% -3.7366%] (p = 0.00 < 0.05)
Performance has improved.
Found 5 outliers among 100 measurements (5.00%)
4 (4.00%) high mild
1 (1.00%) high severe
poseidon_bn254_x5_3 time: [45.248 µs 45.295 µs 45.355 µs]
change: [-2.6216% -2.5166% -2.4136%] (p = 0.00 < 0.05)
Performance has improved.
Found 10 outliers among 100 measurements (10.00%)
4 (4.00%) high mild
6 (6.00%) high severe
poseidon_bn254_x5_4 time: [64.118 µs 64.156 µs 64.196 µs]
change: [-2.4788% -2.2897% -2.1257%] (p = 0.00 < 0.05)
Performance has improved.
Found 4 outliers among 100 measurements (4.00%)
3 (3.00%) high mild
1 (1.00%) high severe
poseidon_bn254_x5_5 time: [83.835 µs 83.908 µs 83.999 µs]
change: [-1.3413% -1.1845% -1.0296%] (p = 0.00 < 0.05)
Performance has improved.
Found 7 outliers among 100 measurements (7.00%)
5 (5.00%) high mild
2 (2.00%) high severe
poseidon_bn254_x5_6 time: [111.90 µs 111.99 µs 112.10 µs]
change: [-2.1066% -1.7171% -1.3534%] (p = 0.00 < 0.05)
Performance has improved.
Found 7 outliers among 100 measurements (7.00%)
6 (6.00%) high mild
1 (1.00%) high severe
poseidon_bn254_x5_7 time: [143.53 µs 143.65 µs 143.79 µs]
change: [-0.9447% -0.7207% -0.5351%] (p = 0.00 < 0.05)
Change within noise threshold.
Found 2 outliers among 100 measurements (2.00%)
1 (1.00%) high mild
1 (1.00%) high severe
poseidon_bn254_x5_8 time: [175.51 µs 175.66 µs 175.83 µs]
change: [-0.3432% -0.1513% +0.1045%] (p = 0.26 > 0.05)
No change in performance detected.
Found 6 outliers among 100 measurements (6.00%)
3 (3.00%) high mild
3 (3.00%) high severe
poseidon_bn254_x5_9 time: [207.51 µs 207.86 µs 208.27 µs]
change: [+0.9627% +1.1469% +1.3456%] (p = 0.00 < 0.05)
Change within noise threshold.
Found 4 outliers among 100 measurements (4.00%)
2 (2.00%) high mild
2 (2.00%) high severe
poseidon_bn254_x5_10 time: [272.48 µs 272.89 µs 273.35 µs]
change: [-1.3056% -0.5531% +0.0423%] (p = 0.12 > 0.05)
No change in performance detected.
Found 7 outliers among 100 measurements (7.00%)
5 (5.00%) high mild
2 (2.00%) high severe
poseidon_bn254_x5_11 time: [297.25 µs 297.69 µs 298.21 µs]
change: [-0.3990% -0.2486% -0.0819%] (p = 0.00 < 0.05)
Change within noise threshold.
Found 6 outliers among 100 measurements (6.00%)
1 (1.00%) high mild
5 (5.00%) high severe
poseidon_bn254_x5_12 time: [376.93 µs 377.70 µs 378.61 µs]
change: [-0.6526% -0.3721% -0.0959%] (p = 0.01 < 0.05)
Change within noise threshold.
Found 9 outliers among 100 measurements (9.00%)
1 (1.00%) high mild
8 (8.00%) high severe
```
e039a09 to
fb624a6
Compare
|
Both of these come from the same root cause: the Circom-specific constants became universal capacities. 1. 2. Generated with Claude Code |
|
Good catch, quite embarrassing from my side. We can focus on landing your PR. |
See individual commits for details.
Current
main:This change + arrayvec migration: