86 lines
4.4 KiB
Markdown
86 lines
4.4 KiB
Markdown
# 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_all`
|
|
0.85 GB/s, `open_all` 1.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 `parallel` feature) 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.
|
|
- **`get` of a large file is slower, on purpose.** Rust fsyncs the output
|
|
file before giving it its name, so a crash just after `get` cannot 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 single `fsync` of 1 GiB of
|
|
freshly written data (measured with `strace -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.
|