Skip to content

Faster Blake3Digest compression #2471

Description

@winne42

I made an accidental discovery while trying to optimize something else: Blake3 as implemented in BC has a lot of potential for performance optimization (up to ~3x on my machine).
Out of scope for this ticket: with Vector API, I saw another ~2.5x improvement easily passing 1 GB/s, but that's another story.

Since I am not an expert I'll just report the results from Claude as they are, so the tokens were not uselessly spent. Maybe a human can review it or at least take it as an inspiration for future optimizations.

Claude's summary below, draft PR is #2472.

Finding

BLAKE3 hashed 16 KB at about 190 MB/s, below BLAKE2b's 406 MB/s on the same machine, although it does 7 rounds per block against BLAKE2b's 12. A JFR profile put 76% of the samples in Blake3Digest.permuteIndices(). compress() kept the whole state in the theV and theM field arrays, fetched every message word through a theIndices byte array (theM[theIndices[i]]), and re-permuted that index array after each round. With all 7 x 8 G mixes going through memory and a double indirection, C2 could not keep the state in registers.

Fix

compress() now loads the 16 state words and 16 message words into local variables and runs the seven rounds on them. Between rounds it applies the fixed BLAKE3 message permutation to the locals
(m'[i] = m[SIGMA[i]]). At the end it writes the state back to theV once. theM is left unchanged, as before.
The index array, SIGMA, mixG, initIndices and permuteIndices are gone. The rest of the class is untouched: buffering, chunk and parent handling, the chaining-value stack, output and Memoable. Also added: a release note under 1.87 "Additional Features and Functionality".

Verification

  • Blake3Test - the official BLAKE3 vectors for hash, keyed hash and derive-key, with extended output - passes.
  • The core crypto.test suite (org.bouncycastle.crypto.test.AllTests) passes.
  • Old and new classes side by side (the main version compiled under another name): 6,000 random digests, 81,506 updates, with identical output. The run covered hash, keyed and derive-key modes, digest and XOF output (read in pieces), Memoable copies taken mid-stream, update(byte), and inputs up to 200 KB fed in random chunks.
  • Checkstyle passes.

Benchmark

JMH average time, old (main) and new implementation in the same run: 2 forks x 5 x 1 s after 5 x 1 s warm-up, update + doFinal of a 256-bit digest. AMD Ryzen 9 5900HX (AVX2), OpenJDK 25.0.4, Linux; 1-minute load 2.5 - 3.6 (one core was held by a runaway desktop process).

Input Before After Speed-up
64 B 388 ns (165 MB/s) 146 ns (438 MB/s) 2.7x
1 KB 5,486 ns (187 MB/s) 1,900 ns (539 MB/s) 2.9x
16 KB ~90,000 ns (182 MB/s) 32,381 ns (506 MB/s) 2.8x

For 16 KB, one fork of the old implementation was disturbed (86 - 136 us per iteration; the mean over both forks was 98,757 +- 23,867 ns). The other fork was steady at about 90 us, which is the figure used above. The new implementation was steady in both forks. At 16 KB BLAKE3 now outruns BLAKE2b here (506 against 406 MB/s).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions