4.4 KiB
Benchmarks: Go 0.5 against Rust 0.6
Measured on 2026-10-08 on one machine: Intel Core i7-7700 (4 cores, 8
threads), 31 GiB RAM, ext4 on a two-disk RAID 1 (/dev/md2), Linux 7.0.
Both binaries were built from this tree: the Go oracle (oracle/go, Go
1.27.1, CGO_ENABLED=0) and the Rust release build (Rust 1.99.0, LTO).
Limitation: the machine was shared. Other users' Docker builds kept the load average between 4 and 12 throughout, so absolute times are noisy. Go and Rust were run alternately under the same conditions, and every figure is a median with its spread, but small differences mean nothing. Page caches were warm (dropping them needs root), so cold-cache numbers were not measured.
Primitives
cargo bench --bench micro and
(cd oracle/go && go test -run XXX -bench Oracle -benchtime 2s ./ ./internal/pqcrypt ./internal/stream),
run alternately twice (load 4.4 to 5.4). Medians in milliseconds; the Rust
harness's p10 to p90 range is in brackets.
| Operation | Go | Rust |
|---|---|---|
| Derive hybrid identity from seed | 5.6 | 3.0 (2.9 to 4.6) |
| Wrap file key (both encapsulations) | 7.4 to 8.0 | 3.1 (3.0 to 4.8) |
| Unwrap file key (both decapsulations) | 7.6 to 8.3 | 3.1 (3.0 to 3.6) |
| Encrypt and decrypt one 4 MiB object | 19.9 to 20.1 | 19.0 to 19.5 |
| STREAM seal and open, 64 MiB, streaming API | 89 to 92 | 187 to 188 |
| Unlock key file (Argon2id for both slots) | 121 to 125 | 129 (125 to 147) |
- The FrodoKEM port is about twice as fast as CIRCL's Go code, mostly from multiplying A row by row instead of walking it by column.
- ChaCha20-Poly1305 is slower: RustCrypto reaches about 1.2 GB/s on this CPU
(
cargo bench --bench aead_probe), Go's assembly about twice that. The 64 MiB row uses the streaming reader and writer, which copy through 64 KiB buffers. The vault itself uses the in-memory path (seal_all0.85 GB/s,open_all1.4 GB/s), and per 4 MiB object the two are level because the KEM work is cheaper. - Unlock takes 180 ms with Argon2id lanes computed in turn. Computing the
four lanes in parallel (argon2's
parallelfeature) brings it to 129 ms, close to Go. The parameters are unchanged.
End to end
RUNS=3 BIG_MB=1024 SMALL=500 scripts/bench.sh (load 8 to 13). Every step
includes unlocking the key. The 1 GiB file and the 500 small files (1 to
16 KiB each) are each added to a fresh vault. The reads use one vault
holding both, plus filler. Times are medians of 3 (min to max).
| Step | Go | Rust |
|---|---|---|
| add 1 GiB file | 41.7 s (32.0 to 50.3) | 29.6 s (28.5 to 31.0) |
| add 500 small files | 123.9 s (113.3 to 156.2) | 79.0 s (77.3 to 133.3) |
| get 1 GiB file | 9.8 s (8.8 to 10.0) | 16.2 s (15.5 to 16.7) |
| list (about 530 records) | 4.95 s (4.89 to 5.20) | 0.74 s (0.71 to 0.77) |
| get one small file | 5.24 s (4.88 to 6.00) | 0.74 s (0.70 to 0.81) |
| Peak RSS | 137 to 140 MiB | 132 MiB |
| Peak threads | 18 | 17 |
| Binary size | 11.5 MB | 3.5 MB (static musl: 3.4 MB) |
- Catalogue reads (
list, and every command that looks a name up) are about 6 times faster. Records are decrypted in parallel on the worker pool (bounded at twice the worker count in flight), and each FrodoKEM decapsulation is cheaper. - Adds are faster. Both implementations fsync every object, its shard directory and every record. Rust overlaps up to eight objects' encryption and syncs on the pool.
getof a large file is slower, on purpose. Rust fsyncs the output file before giving it its name, so a crash just aftergetcannot leave the name pointing at an empty or partial file. Go did not. On an idle rerun (load about 8, output on the same disk), Go took 3.6 s and Rust 15.5 s. Of Rust's time, 13.3 s was the singlefsyncof 1 GiB of freshly written data (measured withstrace -T). The other 2 s or so is unlock, decryption and writing. This is the cost of the durability barrier on this RAID 1 array, and it scales with the disk's write speed.- Peak memory is dominated by Argon2id: 64 MiB for each of the two slots, computed at the same time in both implementations. Rust's object pipeline adds at most eight 4 MiB plaintext buffers and their ciphertexts. Peak threads: Rust has 8 workers, 8 Argon2 threads while unlocking, and the main thread.
Reproduce with scripts/bench.sh (it builds both binaries, creates
synthetic data in target/bench, and prints the tables' numbers) and the
two micro-benchmark commands above.