Skip to content
forkcloserPublic

About

An AVX-512 accelerated implementation of the BLAKE3 cryptographic hash function

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

1 watching

Forks

 
 

Repository files navigation

blake3

go get github.com/forkcloser/blake3

blake3 implements the BLAKE3 cryptographic hash function. The implementation aims to be performant without sacrificing (too much) readability. Requires Go 1.26 or later.

In addition to the pure-Go implementation, this package also contains AVX-512 and AVX2 routines (generated by avo) that greatly increase performance for large inputs and outputs on amd64. Every other architecture runs the pure-Go code.

Three packages:

  • blake3 — hash.Hash (New, Sum256, Sum512, DeriveKey) and the seekable XOF (Hasher.XOF).
  • bao — Bao verified streaming: combined and outboard encodings, slices, and chunk-group verification.
  • guts — the low-level tree-hashing primitives the two packages above are built on, for callers that hash their own trees.

About this fork

A friendly fork of lukechampine/blake3 (import path lukechampine.com/blake3), taken at upstream v1.4.1, which is also upstream's current master. The exported API is upstream's with two additions (guts.CompressBlocksN, bao.MaxGroup) and one removal: the five Bao* wrappers upstream kept in the root package after moving Bao to its own package, already deprecated there, are gone; use package bao. For everything else, switching is a change of import path.

What this fork fixes and adds that upstream does not have, each with the change that landed it, is the table in UPSTREAM.md, beside the fork point and what was taken from upstream since. The root package and bao are held to ports of the specification's reference implementation (contract_test.go, bounded_test.go), and upstream's suites retired under a mutation score; the amd64 assembly is generated from avo/gen.go in its own module, regenerated and compared by just lint-generated, and passes go vet.

Contributions that make sense upstream should go upstream first; see the organization's CONTRIBUTING and SECURITY policies.

Benchmarks

This fork against upstream, same machine

Upstream lukechampine/blake3 v1.4.1 (dd9ffb9, the fork point) and this fork, measured 2026-09-07 with Go 1.26.5 on an Apple M5 Pro (darwin/arm64, so the generic implementation; no SIMD paths run on this host). The two were run interleaved, three rounds of go test -run '^$' -bench . -benchmem -count=2 each, and paired with benchstat; both use the same plain b.N loop, since b.Loop alone costs ~2 ns per iteration and shows up as a false 4% on the 73 ns case. Lower is better; ~ means no statistically significant difference. Upstream's write-size rows were noisier (up to ±51%) than the fork's (≤4%) in this run; the medians agree with two earlier runs.

Benchmark upstream v1.4.1 this fork vs upstream
Write (32 KiB via io.CopyN) 912.0 ns 809.7 ns −11%
XOF/64 1009 ns 3.1 ns −99.7%
XOF/1024 1.004 µs 1.006 µs ~
XOF/65536 37.84 µs 36.10 µs −4.6%
XOF/1048576 223.2 µs 209.5 µs −6.1%
Sum256/64 73.11 ns 72.96 ns ~
Sum256/1024 1.034 µs 1.030 µs ~
Sum256/65536 44.61 µs 38.83 µs −13%
Sum256/1048576 236.5 µs 225.7 µs −4.6%
WriteSizes/4096 9.47 µs 4.70 µs −50%
WriteSizes/8192 14.89 µs 9.39 µs −37%
WriteSizes/16384 20.27 µs 18.21 µs −10%
WriteSizes/24576 27.06 µs 26.93 µs ~
WriteSizes/32768 29.84 µs 25.88 µs −13%
WriteSizes/49152 40.98 µs 31.56 µs −23%
WriteSizes/65536 42.73 µs 38.37 µs −10%
WriteSizes/131072 58.39 µs 53.29 µs −8.7%
WriteSizes/1048576 234.0 µs 223.6 µs −4.4%

Allocations per operation, where either side allocates at all:

Benchmark upstream v1.4.1 this fork
XOF/65536 58 9
XOF/1048576 70 37
Sum256/65536 30 20
Sum256/1048576 171 133
WriteSizes/4096 … /24576 9 – 18 2 – 3
WriteSizes/32768 … /1048576 20 – 170 10 – 132

The XOF/64 row is the OutputReader fix: upstream recomputes the block on every Seek(0)+Read, this fork serves it from its window. WriteSizes is this fork's benchmark (writesizes_bench_test.go), added because BenchmarkWrite alone, at io.Copy's 32 KiB, hid a 27% regression at exactly that size in an earlier version of the write scheduler; the same file compiled against upstream produced the upstream column.

The three amd64 paths, same machine

The fork's own figures, measured 2026-11-01 by just bench-amd64 (the bench workflow, run 41330760868) with Go 1.27.3 on a GitHub ubuntu-24.04 runner whose CPU is an Intel Xeon Platinum 8370C (Ice Lake, 4 vCPUs, AVX-512): the AVX-512 kernels as dispatched, then the same binary with GODEBUG=cpu.avx512f=off (AVX2) and with cpu.avx2=off as well (pure Go). Medians of six runs each; half the range over the median is under 3% everywhere, XOF/64's 2.7% the largest. Lower is better.

Benchmark AVX-512 AVX2 pure Go
Write 1.097 µs 999.7 ns 2.077 µs
WriteSizes/4096 8.537 µs 8.313 µs 7.864 µs
WriteSizes/8192 13.32 µs 13.02 µs 15.72 µs
WriteSizes/16384 18.33 µs 18.01 µs 31.13 µs
WriteSizes/24576 20.94 µs 22.58 µs 46.45 µs
WriteSizes/32768 35.34 µs 31.67 µs 66.44 µs
WriteSizes/49152 38.17 µs 42.12 µs 76.12 µs
WriteSizes/65536 41.18 µs 49.22 µs 92.01 µs
WriteSizes/131072 56.35 µs 69.22 µs 153.9 µs
WriteSizes/1048576 193.3 µs 292.8 µs 967.3 µs
XOF/64 8.547 ns 8.122 ns 8.127 ns
XOF/1024 231.3 ns 299.4 ns 1.79 µs
XOF/65536 20.59 µs 28.91 µs 73.86 µs
XOF/1048576 123.1 µs 164.1 µs 803.6 µs
Sum256/64 118 ns 118.2 ns 118 ns
Sum256/1024 1.788 µs 1.789 µs 1.788 µs
Sum256/65536 43.32 µs 50.55 µs 94.19 µs
Sum256/1048576 189.5 µs 293.3 µs 967.4 µs

Throughput in GB/s, where the input is large enough for the tree to matter:

Benchmark AVX-512 AVX2 pure Go
Write 0.93 1.02 0.49
WriteSizes/65536 1.59 1.33 0.71
WriteSizes/1048576 5.42 3.58 1.08
XOF/65536 3.18 2.27 0.89
XOF/1048576 8.52 6.39 1.30
Sum256/65536 1.51 1.30 0.70
Sum256/1048576 5.53 3.57 1.08

The shape: hashing, the AVX-512 kernels pay from 48 KiB up, 1.5× AVX2 and 5× pure Go at 1 MiB; from 4 to 32 KiB the two SIMD paths trade places within 12%, AVX2 ahead at 32 KiB (Write, at io.Copy's 32 KiB, is AVX2's by 10%), and at 4 KiB the pure Go is the fastest of the three, by 6% over AVX2 and 9% over AVX-512, the kernels' fixed cost not yet amortised. XOF output is the AVX-512 kernels' from the first kilobyte, the block kernel filling sixteen blocks at once. Inputs of a chunk or less (Sum256/64, Sum256/1024, XOF/64) never reach the kernels and are equal on every path. A cloud runner is one machine on one day; the run's raw output is the workflow's bench-amd64 artifact, and another dispatch produces another.

Shortcomings

There is no assembly routine for single-block compressions. This is most noticeable for ~1KB inputs.

Each assembly routine inlines all 7 rounds, causing thousands of lines of duplicated code. Ideally the routines could be merged such that only a single routine is generated for AVX-512 and AVX2, without sacrificing too much performance.

On amd64, the SIMD kernels always process a full 16-chunk (16 KiB) buffer: a shorter eigentree tail is padded and the unused lanes' work discarded.

The AVX-512 kernels have not been executed since the fork. No CI runner has avx512f (the amd64 legs report AVX2 only), the development machine is arm64, and the fork changed the kernels' generator (the scalar broadcasts now go through a register). The AVX2 kernels and the pure-Go code run on every CI leg; the AVX-512 path is pinned by the same vectors but on a host nobody has. TODO: run the test suite on an AVX-512 host, or under an emulator that has it, before the next release that touches avo/gen.go.

About

An AVX-512 accelerated implementation of the BLAKE3 cryptographic hash function

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages