Source
tests/benchmarks/crypto_openssl_bench.cpp
1
// Copyright (c) 2026 BigBrain LLC. MIT-licensed (see LICENSE).2
// Original work; see ACKNOWLEDGMENTS.md for the open-source ideas we build upon.3
// crypto_openssl_bench — cheatah's crypto primitives vs OpenSSL (libcrypto), the de-facto4
// reference implementation. Digests, HMAC, and the two TLS 1.3 AEADs are timed on the same5
// 4 KiB payload (a typical TLS record) so the rows are directly comparable. The OpenSSL6
// comparison rows compile only when libcrypto headers are present (CHEATAH_HAVE_OPENSSL,7
// set by the benchmark CMake when find_package(OpenSSL) succeeds); otherwise only the8
// cheatah rows build, so the benchmark never becomes a hard dependency.9
//10
// Run with the release preset:11
// cmake --build --preset release-benchmarks12
// ./build/release/bin/cheatah_benchmarks --benchmark_filter='Crypto'13
#include <benchmark/benchmark.h>15
#include <string>16
#include <vector>18
#include "bench_labels.hpp"20
#include "aead.hpp"21
#include "hashlib.hpp"23
#ifdef CHEATAH_HAVE_OPENSSL24
#include <openssl/evp.h>25
#include <openssl/hmac.h>26
#endif28
namespace {30
std::string fill(std::size_t n, unsigned seed) {31
std::string s;32
s.reserve(n);33
for (std::size_t i = 0; i < n; ++i) s.push_back(static_cast<char>((i * 131u + seed) & 0xFF));34
return s;35
}37
const std::string kData = fill(4096, 7); // 4 KiB payload38
const std::string kKey = fill(32, 19); // raw HMAC key39
const std::string kAad = "x-record-header"; // associated data41
// AEAD takes HEX key/nonce. 32-byte key (ChaCha20 / two AES-128 keys), 16-byte AES-128 key,42
// 12-byte nonce. The matching RAW bytes feed OpenSSL's EVP API.43
std::string to_hex(const std::string& raw) {44
static constexpr char H[] = "0123456789abcdef";45
std::string o;46
o.reserve(raw.size() * 2);47
for (unsigned char c : raw) { o.push_back(H[c >> 4]); o.push_back(H[c & 0xF]); }48
return o;49
}50
const std::string kKey32 = fill(32, 23);51
const std::string kKey16 = fill(16, 29);52
const std::string kNonce = fill(12, 31);53
const std::string kKey32Hex = to_hex(kKey32);54
const std::string kKey16Hex = to_hex(kKey16);55
const std::string kNonceHex = to_hex(kNonce);57
} // namespace59
// ---------------- SHA-256 ----------------60
static void BM_CryptoSha256_Cheatah(benchmark::State& s) {61
for (auto _ : s) benchmark::DoNotOptimize(cheatah::hashlib::sha256_digest(kData));62
s.SetBytesProcessed(static_cast<int64_t>(s.iterations()) * kData.size());63
}65
// ---------------- SHA-512 ----------------66
static void BM_CryptoSha512_Cheatah(benchmark::State& s) {67
for (auto _ : s) benchmark::DoNotOptimize(cheatah::hashlib::sha512_digest(kData));68
s.SetBytesProcessed(static_cast<int64_t>(s.iterations()) * kData.size());69
}71
// ---------------- HMAC-SHA256 ----------------72
static void BM_CryptoHmacSha256_Cheatah(benchmark::State& s) {73
for (auto _ : s) benchmark::DoNotOptimize(cheatah::hashlib::hmac_sha256(kKey, kData));74
s.SetBytesProcessed(static_cast<int64_t>(s.iterations()) * kData.size());75
}77
// ---------------- ChaCha20-Poly1305 ----------------78
static void BM_CryptoChaCha20Poly1305_Cheatah(benchmark::State& s) {79
for (auto _ : s)80
benchmark::DoNotOptimize(81
cheatah::aead::chacha20poly1305_encrypt(kKey32Hex, kNonceHex, kAad, kData));82
s.SetBytesProcessed(static_cast<int64_t>(s.iterations()) * kData.size());83
}85
// The allocation-free form, measured against the row above. Same algorithm and the same code86
// paths — the only difference is that this one neither allocates its result nor assembles a87
// MAC-input buffer, so the gap is exactly the cost those two allocations were adding.88
static void BM_CryptoChaCha20Poly1305_Cheatah_Into(benchmark::State& s) {89
unsigned char key[32], nonce[12];90
for (int i = 0; i < 32; ++i) key[i] = static_cast<unsigned char>(i);91
for (int i = 0; i < 12; ++i) nonce[i] = static_cast<unsigned char>(i);92
std::vector<unsigned char> out(kData.size() + 16);93
for (auto _ : s) {94
benchmark::DoNotOptimize(cheatah::aead::chacha20poly1305_encrypt_into(95
key, nonce, reinterpret_cast<const unsigned char*>(kAad.data()), kAad.size(),96
reinterpret_cast<const unsigned char*>(kData.data()), kData.size(), out.data()));97
}98
s.SetBytesProcessed(static_cast<int64_t>(s.iterations()) * kData.size());99
}101
// ---------------- AES-128-GCM ----------------102
static void BM_CryptoAes128Gcm_Cheatah(benchmark::State& s) {103
for (auto _ : s)104
benchmark::DoNotOptimize(105
cheatah::aead::aes128gcm_encrypt(kKey16Hex, kNonceHex, kAad, kData));106
s.SetBytesProcessed(static_cast<int64_t>(s.iterations()) * kData.size());107
}109
#ifdef CHEATAH_HAVE_OPENSSL110
namespace {111
// One-shot AEAD encrypt with OpenSSL's EVP interface (ciphertext || 16-byte tag).112
std::string ossl_aead(const EVP_CIPHER* cipher, const std::string& key, const std::string& nonce,113
const std::string& aad, const std::string& pt) {114
EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();115
std::string out(pt.size() + 16, '\0');116
int len = 0, total = 0;117
EVP_EncryptInit_ex(ctx, cipher, nullptr,118
reinterpret_cast<const unsigned char*>(key.data()),119
reinterpret_cast<const unsigned char*>(nonce.data()));120
EVP_EncryptUpdate(ctx, nullptr, &len, reinterpret_cast<const unsigned char*>(aad.data()),121
static_cast<int>(aad.size()));122
EVP_EncryptUpdate(ctx, reinterpret_cast<unsigned char*>(&out[0]), &len,123
reinterpret_cast<const unsigned char*>(pt.data()), static_cast<int>(pt.size()));124
total = len;125
EVP_EncryptFinal_ex(ctx, reinterpret_cast<unsigned char*>(&out[0]) + total, &len);126
total += len;127
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_GET_TAG, 16,128
reinterpret_cast<unsigned char*>(&out[0]) + total);129
EVP_CIPHER_CTX_free(ctx);130
return out;131
}132
} // namespace134
static void BM_CryptoSha256_OpenSSL(benchmark::State& s) {135
unsigned char md[32];136
unsigned int n;137
for (auto _ : s) {138
EVP_Digest(kData.data(), kData.size(), md, &n, EVP_sha256(), nullptr);139
benchmark::DoNotOptimize(md);140
}141
s.SetBytesProcessed(static_cast<int64_t>(s.iterations()) * kData.size());142
}144
static void BM_CryptoSha512_OpenSSL(benchmark::State& s) {145
unsigned char md[64];146
unsigned int n;147
for (auto _ : s) {148
EVP_Digest(kData.data(), kData.size(), md, &n, EVP_sha512(), nullptr);149
benchmark::DoNotOptimize(md);150
}151
s.SetBytesProcessed(static_cast<int64_t>(s.iterations()) * kData.size());152
}154
static void BM_CryptoHmacSha256_OpenSSL(benchmark::State& s) {155
unsigned char md[32];156
unsigned int n;157
for (auto _ : s) {158
HMAC(EVP_sha256(), kKey.data(), static_cast<int>(kKey.size()),159
reinterpret_cast<const unsigned char*>(kData.data()), kData.size(), md, &n);160
benchmark::DoNotOptimize(md);161
}162
s.SetBytesProcessed(static_cast<int64_t>(s.iterations()) * kData.size());163
}165
static void BM_CryptoChaCha20Poly1305_OpenSSL(benchmark::State& s) {166
for (auto _ : s)167
benchmark::DoNotOptimize(ossl_aead(EVP_chacha20_poly1305(), kKey32, kNonce, kAad, kData));168
s.SetBytesProcessed(static_cast<int64_t>(s.iterations()) * kData.size());169
}171
static void BM_CryptoAes128Gcm_OpenSSL(benchmark::State& s) {172
for (auto _ : s)173
benchmark::DoNotOptimize(ossl_aead(EVP_aes_128_gcm(), kKey16, kNonce, kAad, kData));174
s.SetBytesProcessed(static_cast<int64_t>(s.iterations()) * kData.size());175
}176
#endif // CHEATAH_HAVE_OPENSSL178
// ---- registration: each pair back-to-back ------------------------------------------179
//180
// Order matters, and it is the whole reason these registrations are not next to their181
// functions. Google Benchmark runs registrations in order, so when all six cheatah rows182
// were registered first and all five OpenSSL rows after, a row and its twin were measured183
// minutes apart under the gate profile (15 repetitions x 0.5 s). Any clock or thermal184
// drift over that window lands entirely on one side of the ratio — which is exactly the185
// bias the comparison exists to avoid. Registering each pair adjacently puts the two sides186
// seconds apart instead. (--benchmark_enable_random_interleaving then scatters the187
// repetitions, turning what remains into zero-mean noise.)188
CHEATAH_BENCH_LABEL("BM_CryptoSha256", "SHA-256");189
BENCHMARK(BM_CryptoSha256_Cheatah);190
#ifdef CHEATAH_HAVE_OPENSSL191
BENCHMARK(BM_CryptoSha256_OpenSSL);192
#endif194
CHEATAH_BENCH_LABEL("BM_CryptoSha512", "SHA-512");195
BENCHMARK(BM_CryptoSha512_Cheatah);196
#ifdef CHEATAH_HAVE_OPENSSL197
BENCHMARK(BM_CryptoSha512_OpenSSL);198
#endif200
CHEATAH_BENCH_LABEL("BM_CryptoHmacSha256", "HMAC-SHA256");201
BENCHMARK(BM_CryptoHmacSha256_Cheatah);202
#ifdef CHEATAH_HAVE_OPENSSL203
BENCHMARK(BM_CryptoHmacSha256_OpenSSL);204
#endif206
CHEATAH_BENCH_LABEL("BM_CryptoChaCha20Poly1305", "ChaCha20-Poly1305");207
BENCHMARK(BM_CryptoChaCha20Poly1305_Cheatah);208
#ifdef CHEATAH_HAVE_OPENSSL209
BENCHMARK(BM_CryptoChaCha20Poly1305_OpenSSL);210
#endif212
// The allocation-free variant: OURS, not a rival (bench_pairs.hpp reads the side token213
// before the `_Into` variant tag). It has no OpenSSL twin, so it reports on its own.214
CHEATAH_BENCH_LABEL("BM_CryptoChaCha20Poly1305_Into", "ChaCha20-Poly1305 (allocation-free)");215
BENCHMARK(BM_CryptoChaCha20Poly1305_Cheatah_Into);217
CHEATAH_BENCH_LABEL("BM_CryptoAes128Gcm", "AES-128-GCM (AES-NI + PCLMULQDQ)");218
BENCHMARK(BM_CryptoAes128Gcm_Cheatah);219
#ifdef CHEATAH_HAVE_OPENSSL220
BENCHMARK(BM_CryptoAes128Gcm_OpenSSL);221
#endif