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.
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.
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 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.
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.