Skip to content

perf: Avoid heap allocations in state, input buffers and byte hash path - #61

Draft
vadorovsky wants to merge 2 commits into
Lightprotocol:mainfrom
vadorovsky:vad/arrayvec
Draft

vadorovsky wants to merge 2 commits into
Lightprotocol:mainfrom
vadorovsky:vad/arrayvec

Conversation

@vadorovsky

Copy link
Copy Markdown
Contributor

See individual commits for details.

Current main:

poseidon_bn254_x5_1     time:   [24.407 µs 24.626 µs 24.894 µs]
poseidon_bn254_x5_2     time:   [34.412 µs 34.441 µs 34.485 µs]
poseidon_bn254_x5_3     time:   [47.040 µs 47.303 µs 47.542 µs]
poseidon_bn254_x5_4     time:   [65.993 µs 66.204 µs 66.439 µs]
poseidon_bn254_x5_5     time:   [85.580 µs 85.980 µs 86.484 µs]
poseidon_bn254_x5_6     time:   [112.77 µs 112.91 µs 113.08 µs]
poseidon_bn254_x5_7     time:   [143.64 µs 143.69 µs 143.76 µs]
poseidon_bn254_x5_8     time:   [175.33 µs 175.41 µs 175.48 µs]
poseidon_bn254_x5_9     time:   [215.06 µs 223.51 µs 231.91 µs]
poseidon_bn254_x5_10    time:   [274.29 µs 274.95 µs 275.64 µs]
poseidon_bn254_x5_11    time:   [297.08 µs 297.61 µs 298.29 µs]
poseidon_bn254_x5_12    time:   [374.82 µs 375.18 µs 375.67 µs]

This change + arrayvec migration:

poseidon_bn254_x5_1     time:   [22.651 µs 22.673 µs 22.700 µs]
poseidon_bn254_x5_2     time:   [33.034 µs 33.140 µs 33.285 µs]
poseidon_bn254_x5_3     time:   [44.899 µs 44.931 µs 44.976 µs]
poseidon_bn254_x5_4     time:   [64.334 µs 64.487 µs 64.700 µs]
poseidon_bn254_x5_5     time:   [83.545 µs 83.695 µs 83.888 µs]
poseidon_bn254_x5_6     time:   [113.27 µs 113.38 µs 113.54 µs]
poseidon_bn254_x5_7     time:   [143.47 µs 144.17 µs 145.06 µs]
poseidon_bn254_x5_8     time:   [175.51 µs 175.62 µs 175.73 µs]
poseidon_bn254_x5_9     time:   [206.25 µs 206.52 µs 206.83 µs]
poseidon_bn254_x5_10    time:   [272.00 µs 272.19 µs 272.54 µs]
poseidon_bn254_x5_11    time:   [296.73 µs 297.05 µs 297.51 µs]
poseidon_bn254_x5_12    time:   [375.78 µs 376.22 µs 376.66 µs]

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.407 µs 24.626 µs 24.894 µs]
poseidon_bn254_x5_2     time:   [34.412 µs 34.441 µs 34.485 µs]
poseidon_bn254_x5_3     time:   [47.040 µs 47.303 µs 47.542 µs]
poseidon_bn254_x5_4     time:   [65.993 µs 66.204 µs 66.439 µs]
poseidon_bn254_x5_5     time:   [85.580 µs 85.980 µs 86.484 µs]
poseidon_bn254_x5_6     time:   [112.77 µs 112.91 µs 113.08 µs]
poseidon_bn254_x5_7     time:   [143.64 µs 143.69 µs 143.76 µs]
poseidon_bn254_x5_8     time:   [175.33 µs 175.41 µs 175.48 µs]
poseidon_bn254_x5_9     time:   [215.06 µs 223.51 µs 231.91 µs]
poseidon_bn254_x5_10    time:   [274.29 µs 274.95 µs 275.64 µs]
poseidon_bn254_x5_11    time:   [297.08 µs 297.61 µs 298.29 µs]
poseidon_bn254_x5_12    time:   [374.82 µs 375.18 µs 375.67 µs]
```

This change + arrayvec migration:

```
poseidon_bn254_x5_1     time:   [22.651 µs 22.673 µs 22.700 µs]
poseidon_bn254_x5_2     time:   [33.034 µs 33.140 µs 33.285 µs]
poseidon_bn254_x5_3     time:   [44.899 µs 44.931 µs 44.976 µs]
poseidon_bn254_x5_4     time:   [64.334 µs 64.487 µs 64.700 µs]
poseidon_bn254_x5_5     time:   [83.545 µs 83.695 µs 83.888 µs]
poseidon_bn254_x5_6     time:   [113.27 µs 113.38 µs 113.54 µs]
poseidon_bn254_x5_7     time:   [143.47 µs 144.17 µs 145.06 µs]
poseidon_bn254_x5_8     time:   [175.51 µs 175.62 µs 175.73 µs]
poseidon_bn254_x5_9     time:   [206.25 µs 206.52 µs 206.83 µs]
poseidon_bn254_x5_10    time:   [272.00 µs 272.19 µs 272.54 µs]
poseidon_bn254_x5_11    time:   [296.73 µs 297.05 µs 297.51 µs]
poseidon_bn254_x5_12    time:   [375.78 µs 376.22 µs 376.66 µs]
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant