1. Scenarios

December 18, 2023 ยท View on GitHub

This repository contains the results and analysis after benchmarking the rustls library in different configurations. It also includes benchmarks of OpenSSL, which are interesting because rustls is aiming to be an OpenSSL replacement. The benchmarks are based on previous work by @ctz, who originally benchmarked rustls against OpenSSL in 2019 and published the results online.

1. Scenarios

The benchmarks focus on the following aspects of performance:

  1. Bulk data transfer throughput in MB/s;
  2. Handshake throughput (full, session id, tickets) in handshakes per second;
  3. Memory usage per connection.

For bulk transfers, we benchmark the following cipher suites: AES128-GCM_SHA256, AES256-GCM_SHA384 and CHACHA20_POLY1305. We test them all using TLS 1.2, and additionally test AES256-GCM_SHA384 using TLS 1.3 as a sanity check (performance should be a tiny bit better than TLS 1.2).

For handshakes, we benchmark ECDHE + RSA using a 2048-bit key. We use both TLS 1.2 and TLS 1.3.

As an example, see below a comparison between OpenSSL and the aws-lc rustls configuration, showing the speedup / slowdown factor in the rightmost column:

Toggle comparison
ScenarioOpenSSL (3.2.0)Rustls (0.22.0, aws-lc)Factor
bulk_1.2_ECDHE_RSA_AES128-GCM_SHA256_sent6503.586712.291.03x
bulk_1.2_ECDHE_RSA_AES128-GCM_SHA256_received7234.116017.820.83x
bulk_1.2_ECDHE_RSA_AES256-GCM_SHA384_sent6162.576193.181.00x
bulk_1.2_ECDHE_RSA_AES256-GCM_SHA384_received6609.275669.770.86x
bulk_1.2_ECDHE_RSA_CHACHA20_POLY1305_sent2998.061750.110.58x
bulk_1.2_ECDHE_RSA_CHACHA20_POLY1305_received3107.171731.270.56x
bulk_1.3_ECDHE_RSA_AES256-GCM_SHA384_sent6041.826256.941.04x
bulk_1.3_ECDHE_RSA_AES256-GCM_SHA384_received6406.445945.020.93x
handshake-full_1.2_ECDHE_RSA_AES256-GCM_SHA384_client2581.164991.731.93x
handshake-full_1.2_ECDHE_RSA_AES256-GCM_SHA384_server2120.951485.950.70x
handshake-session-id_1.2_ECDHE_RSA_AES256-GCM_SHA384_client21514.449893.992.32x
handshake-session-id_1.2_ECDHE_RSA_AES256-GCM_SHA384_server23183.841846.341.80x
handshake-ticket_1.2_ECDHE_RSA_AES256-GCM_SHA384_client21491.347419.842.21x
handshake-ticket_1.2_ECDHE_RSA_AES256-GCM_SHA384_server20190.136518.871.81x
handshake-full_1.3_ECDHE_RSA_AES256-GCM_SHA384_client2273.574683.462.06x
handshake-full_1.3_ECDHE_RSA_AES256-GCM_SHA384_server1874.11362.790.73x
handshake-session-id_1.3_ECDHE_RSA_AES256-GCM_SHA384_client3415.3511272.983.30x
handshake-session-id_1.3_ECDHE_RSA_AES256-GCM_SHA384_server3060.177434.412.43x
handshake-ticket_1.3_ECDHE_RSA_AES256-GCM_SHA384_client5182.5111244.792.17x
handshake-ticket_1.3_ECDHE_RSA_AES256-GCM_SHA384_server4718.397347.911.56x

2. Methodology

System configuration

We ran the benchmarks on a bare-metal server with the following characteristics:

  • OS: Debian 12 (Bookworm).
  • C/C++ toolchains: GCC 12.2.0 and Clang 14.0.6.
  • CPU: Xeon E-2386G (supporting AVX-512).
  • Memory: 32GB.
  • Extra configuration: hyper-threading disabled, dynamic frequency scaling disabled, cpu scaling governor set to performance for all cores.

OpenSSL

The most recent version of OpenSSL at the time of this research was 3.2.0. We benchmarked it using ctz's openssl-bench repository, which expects a built OpenSSL tree in ../openssl/ and the rustls repository in ../rustls. We ran the benchmarks using BENCH_MULTIPLIER=8 setarch -R make measure (setarch is used here and elsewhere to disable ASLR and thereby reduce noise).

rustls

The most recent version of rustls at the time of this research was 0.22.0. We benchmarked it using bench.rs, included in the repository. We ran it using BENCH_MULTIPLIER=8 setarch -R make -f admin/bench-measure.mk measure (the makefile assumes that the bench.rs binary has already been built, see below for details).

When building rustls, we tested a matrix of configurations involving 3 variables:

  • Cryptography backend: ring 0.17.7 vs aws-lc-rs 1.5.2.
  • C/C++ toolchain: GCC 12.2.0 vs Clang 14.0.6.
  • Allocator: glibc malloc vs jemalloc.

The cryptography backend was specified through cargo features:

  • Build with ring: cargo build -p rustls --release --example bench.
  • Build with aws-lc-rs: cargo build -p rustls --release --example bench --no-default-features --features tls12,aws_lc_rs.

The C/C++ toolchain was specified through Debian's update-alternatives utility. We manually checked the build artifacts of ring and aws-lc-rs to ensure they were indeed built with the right toolchain (using objdump -s --section .comment target/release/deps/artifact.rlib).

The allocator was specified by manually modifying bench.rs to use Jemallocator (requires cargo add --dev jemallocator -p rustls):

#[global_allocator]
static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc;

Result collection and reporting

For each OpenSSL and rustls configuration, we ran the benchmarks 30 times and stored their stdout output in data. We experimentally settled on the median as the value used for reporting due to its stability (taking the maximum or the average was more prone to noise).

Check out our Python notebook in notebook.ipynb, which has code to generate comparison tables in markdown format (and could be modified to generate CSV or something else that suits your needs). Hint: use poetry install --no-root to generate a virtual environment and Visual Studio Code to play with the notebook.

The memory benchmarks are much more deterministic, so we only ran them once.

3. Conclusions

Highlighted results from our benchmarks:

  • Rustls achieves best overall performance when used together with the aws-lc-rs cryptography provider insead of *ring* (see this table). Also, jemalloc offers significantly higher data transfer throughput over Rust's default glibc malloc (35% to 136% higher, depending on the crypto backend and cipher suite). The cause seems to be a massive difference in page faults. See ring and aws-lc tables for details.
  • Rustls uses significantly less memory than OpenSSL. At peak, a Rustls session costs ~13KiB and an OpenSSL session costs ~69KiB in the tested workloads. We measured a C10K memory usage of 132MiB for Rustls and 688MiB for OpenSSL (see this table for details).
  • Rustls offers roughly the same data send throughput as OpenSSL when using AES-based cipher suites. Data receive throughput is 7% to 17% lower, due to a limitation in the Rustls API that forces an extra copy. Work is ongoing to make that copy unnecessary. See this table for the detailed comparison.
  • Rustls offers around 45% less data transfer throughput than OpenSSL when using ChaCha20-based cipher suites (see this table). Further research reveals that OpenSSL's underlying cryptographic primitives are better optimized for server-grade hardware by taking advantage of AVX-512 support (disabling AVX-512 results in similar performance between Rustls and OpenSSL). Curiously, OpenSSL compiled with Clang degrades to the same throughput levels as when AVX-512 is disabled (as reported here).
  • Rustls handles 30% (TLS 1.2) or 27% (TLS 1.3) less full RSA handshakes per second on the server side, but offers significantly more throughput on the client side (up to 106% more, that is, a factor of 2.06x). See this table for details. These differences are presumably due to the underlying RSA implementation, since the situation is reversed when using ECDSA (Rustls beats OpenSSL by a wide margin in server-side performance, and lags behind a bit in client-side performance).
  • Rustls handles 80% to 330% (depending on the scenario) more resumed handshakes per second, either using session ID or ticket-based resumption. See this table for details.

Additional results that might be interesting:

  • For rustls + ring, Clang offers significantly better handshake throughput over GCC (up to 15% in the full handshake scenarios, and up to 27% in the resumed handshake scenarios). See table for details.
  • Overall, rustls + aws-lc beats rustls + ring by a wide margin in bulk transfers (up to 67%), but it lags behind a bit in TLS 1.3 handshakes (up to 16%). See table for details.
  • There is no significant performance difference for rustls + aws-lc between GCC and Clang.

Additional thoughts:

  • aws-lc has its own benchmarking suite which reports that performance is on par with OpenSSL for AES-128-GCM and AES-256-GCM, suggesting that any bottlenecks for AES-based ciphers are elsewhere (in the aws-lc-rs wrapper or in rustls itself). See data/aws-lc-bench-results for the raw output.
  • According to the same aws-lc benchmarks, the bottleneck for ChaCha20-based ciphers is in aws-lc itself.

4. Appendix: result comparisons

jemalloc's effect on rustls + ring bulk transfers

ScenarioRustls (0.22.0, ring_clang, malloc)Rustls (0.22.0, ring_clang, jemalloc)Factor
bulk_1.2_ECDHE_RSA_AES128-GCM_SHA256_sent2281.584150.531.82x
bulk_1.2_ECDHE_RSA_AES128-GCM_SHA256_received4161.154104.50.99x
bulk_1.2_ECDHE_RSA_AES256-GCM_SHA384_sent2144.813700.621.73x
bulk_1.2_ECDHE_RSA_AES256-GCM_SHA384_received3710.153660.960.99x
bulk_1.2_ECDHE_RSA_CHACHA20_POLY1305_sent1291.561739.361.35x
bulk_1.2_ECDHE_RSA_CHACHA20_POLY1305_received1739.01732.481.00x
bulk_1.3_ECDHE_RSA_AES256-GCM_SHA384_sent2141.693738.281.75x

jemalloc's effect on rustls + aws-lc bulk transfers

ScenarioRustls (0.22.0, aws-lc_gcc, malloc)Rustls (0.22.0, aws-lc_gcc, jemalloc)Factor
bulk_1.2_ECDHE_RSA_AES128-GCM_SHA256_sent2847.446712.292.36x
bulk_1.2_ECDHE_RSA_AES128-GCM_SHA256_received6135.096017.820.98x
bulk_1.2_ECDHE_RSA_AES256-GCM_SHA384_sent2788.356193.182.22x
bulk_1.2_ECDHE_RSA_AES256-GCM_SHA384_received5776.345669.770.98x
bulk_1.2_ECDHE_RSA_CHACHA20_POLY1305_sent1287.941750.111.36x
bulk_1.2_ECDHE_RSA_CHACHA20_POLY1305_received1738.971731.271.00x
bulk_1.3_ECDHE_RSA_AES256-GCM_SHA384_sent2791.746256.942.24x
bulk_1.3_ECDHE_RSA_AES256-GCM_SHA384_received6093.485945.020.98x

clang's effect on rustls + ring handshakes

ScenarioRustls (0.22.0, ring_gcc, jemalloc)Rustls (0.22.0, ring_clang, jemalloc)Factor
handshake-full_1.2_ECDHE_RSA_AES256-GCM_SHA384_client3671.064208.91.15x
handshake-full_1.2_ECDHE_RSA_AES256-GCM_SHA384_server1427.561494.011.05x
handshake-session-id_1.2_ECDHE_RSA_AES256-GCM_SHA384_client47940.5947471.950.99x
handshake-session-id_1.2_ECDHE_RSA_AES256-GCM_SHA384_server42929.4541969.720.98x
handshake-ticket_1.2_ECDHE_RSA_AES256-GCM_SHA384_client45656.7145306.780.99x
handshake-ticket_1.2_ECDHE_RSA_AES256-GCM_SHA384_server39121.8938510.330.98x
handshake-full_1.3_ECDHE_RSA_AES256-GCM_SHA384_client4060.864216.281.04x
handshake-full_1.3_ECDHE_RSA_AES256-GCM_SHA384_server1373.621433.711.04x
handshake-session-id_1.3_ECDHE_RSA_AES256-GCM_SHA384_client12008.7512616.71.05x
handshake-session-id_1.3_ECDHE_RSA_AES256-GCM_SHA384_server7067.548944.51.27x
handshake-ticket_1.3_ECDHE_RSA_AES256-GCM_SHA384_client12031.7512615.561.05x
handshake-ticket_1.3_ECDHE_RSA_AES256-GCM_SHA384_server7036.618883.551.26x

clang's effect on OpenSSL bulk transfers

ScenarioOpenSSL (3.2.0_gcc)OpenSSL (3.2.0_clang)Factor
bulk_1.2_ECDHE_RSA_AES128-GCM_SHA256_sent6503.586376.770.98x
bulk_1.2_ECDHE_RSA_AES128-GCM_SHA256_received7234.117055.490.98x
bulk_1.2_ECDHE_RSA_AES256-GCM_SHA384_sent6162.576140.831.00x
bulk_1.2_ECDHE_RSA_AES256-GCM_SHA384_received6609.276671.441.01x
bulk_1.2_ECDHE_RSA_CHACHA20_POLY1305_sent2998.061676.110.56x
bulk_1.2_ECDHE_RSA_CHACHA20_POLY1305_received3107.171708.90.55x
bulk_1.3_ECDHE_RSA_AES256-GCM_SHA384_sent6041.826064.641.00x
bulk_1.3_ECDHE_RSA_AES256-GCM_SHA384_received6406.446405.741.00x

ring vs aws-lc

ScenarioRustls (0.22.0, ring_clang, jemalloc)Rustls (0.22.0, aws-lc_clang, jemalloc)Factor
bulk_1.2_ECDHE_RSA_AES128-GCM_SHA256_sent4150.536573.11.58x
bulk_1.2_ECDHE_RSA_AES128-GCM_SHA256_received4104.56036.71.47x
bulk_1.2_ECDHE_RSA_AES256-GCM_SHA384_sent3700.626171.021.67x
bulk_1.2_ECDHE_RSA_AES256-GCM_SHA384_received3660.965680.811.55x
bulk_1.2_ECDHE_RSA_CHACHA20_POLY1305_sent1739.361740.761.00x
bulk_1.2_ECDHE_RSA_CHACHA20_POLY1305_received1732.481731.351.00x
bulk_1.3_ECDHE_RSA_AES256-GCM_SHA384_sent3738.286225.51.67x
bulk_1.3_ECDHE_RSA_AES256-GCM_SHA384_received3657.535987.831.64x
handshake-full_1.2_ECDHE_RSA_AES256-GCM_SHA384_client4208.94959.081.18x
handshake-full_1.2_ECDHE_RSA_AES256-GCM_SHA384_server1494.011481.990.99x
handshake-session-id_1.2_ECDHE_RSA_AES256-GCM_SHA384_client47471.9550580.451.07x
handshake-session-id_1.2_ECDHE_RSA_AES256-GCM_SHA384_server41969.7242275.271.01x
handshake-ticket_1.2_ECDHE_RSA_AES256-GCM_SHA384_client45306.7848077.081.06x
handshake-ticket_1.2_ECDHE_RSA_AES256-GCM_SHA384_server38510.3336683.260.95x
handshake-full_1.3_ECDHE_RSA_AES256-GCM_SHA384_client4216.284680.921.11x
handshake-full_1.3_ECDHE_RSA_AES256-GCM_SHA384_server1433.711363.290.95x
handshake-session-id_1.3_ECDHE_RSA_AES256-GCM_SHA384_client12616.711522.160.91x
handshake-session-id_1.3_ECDHE_RSA_AES256-GCM_SHA384_server8944.57529.760.84x
handshake-ticket_1.3_ECDHE_RSA_AES256-GCM_SHA384_client12615.5611519.540.91x
handshake-ticket_1.3_ECDHE_RSA_AES256-GCM_SHA384_server8883.557482.640.84x

OpenSSL vs rustls + aws-lc

ScenarioOpenSSL (3.2.0)Rustls (0.22.0, aws-lc_gcc, jemalloc)Factor
bulk_1.2_ECDHE_RSA_AES128-GCM_SHA256_sent6503.586712.291.03x
bulk_1.2_ECDHE_RSA_AES128-GCM_SHA256_received7234.116017.820.83x
bulk_1.2_ECDHE_RSA_AES256-GCM_SHA384_sent6162.576193.181.00x
bulk_1.2_ECDHE_RSA_AES256-GCM_SHA384_received6609.275669.770.86x
bulk_1.2_ECDHE_RSA_CHACHA20_POLY1305_sent2998.061750.110.58x
bulk_1.2_ECDHE_RSA_CHACHA20_POLY1305_received3107.171731.270.56x
bulk_1.3_ECDHE_RSA_AES256-GCM_SHA384_sent6041.826256.941.04x
bulk_1.3_ECDHE_RSA_AES256-GCM_SHA384_received6406.445945.020.93x
handshake-full_1.2_ECDHE_RSA_AES256-GCM_SHA384_client2581.164991.731.93x
handshake-full_1.2_ECDHE_RSA_AES256-GCM_SHA384_server2120.951485.950.70x
handshake-session-id_1.2_ECDHE_RSA_AES256-GCM_SHA384_client21514.449893.992.32x
handshake-session-id_1.2_ECDHE_RSA_AES256-GCM_SHA384_server23183.841846.341.80x
handshake-ticket_1.2_ECDHE_RSA_AES256-GCM_SHA384_client21491.347419.842.21x
handshake-ticket_1.2_ECDHE_RSA_AES256-GCM_SHA384_server20190.136518.871.81x
handshake-full_1.3_ECDHE_RSA_AES256-GCM_SHA384_client2273.574683.462.06x
handshake-full_1.3_ECDHE_RSA_AES256-GCM_SHA384_server1874.11362.790.73x
handshake-session-id_1.3_ECDHE_RSA_AES256-GCM_SHA384_client3415.3511272.983.30x
handshake-session-id_1.3_ECDHE_RSA_AES256-GCM_SHA384_server3060.177434.412.43x
handshake-ticket_1.3_ECDHE_RSA_AES256-GCM_SHA384_client5182.5111244.792.17x
handshake-ticket_1.3_ECDHE_RSA_AES256-GCM_SHA384_server4718.397347.911.56x

OpenSSL vs rustls + aws-lc memory usage

Peak memory usage of a process which creates N sessions (N/2 client sessions associated with N/2 server sessions) and then takes these sessions through a full handshake in lockstep.

Cipher suiteNOpenSSL 3.2.0 (KiB)Rustls 0.22.0, aws-lc_gcc, jemalloc (KiB)vs.
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (TLS 1.2)1001586011540-27%
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (TLS 1.2)10008330021044-75%
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (TLS 1.2)500038247657124-85%
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (TLS 1.2)10000756380104932-86%
TLS_AES_256_GCM_SHA384 (TLS 1.3)1001554411536-0.26
TLS_AES_256_GCM_SHA384 (TLS 1.3)10007671221792-72%
TLS_AES_256_GCM_SHA384 (TLS 1.3)500034864070728-80%
TLS_AES_256_GCM_SHA384 (TLS 1.3)10000688436131816-81%

Note that, when the number of handshakes is small, the difference between Rustls and OpenSSL narrows. This is due to jemalloc reserving more memory than strictly necessary.