/* * Copyright (c) 2024, Ali Mohammad Pur * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define ALL_ALGORITHMS(E) \ E(md5, hash, Hash::MD5) \ E(sha1, hash, Hash::SHA1) \ E(sha256, hash, Hash::SHA256) \ E(sha512, hash, Hash::SHA512) \ E(blake2b, hash, Hash::BLAKE2b) \ E(adler32, checksum, Checksum::Adler32) \ E(crc32, checksum, Checksum::CRC32) \ E(hmac_md5, auth, Authentication::HMAC) \ E(hmac_sha1, auth, Authentication::HMAC) \ E(hmac_sha256, auth, Authentication::HMAC) \ E(hmac_sha512, auth, Authentication::HMAC) \ E(poly1305, auth, Authentication::Poly1305) \ E(ghash, auth, Authentication::GHash) \ E(aes_128_cbc, cipher, Cipher::AESCipher::CBCMode, 128) \ E(aes_128_ctr, cipher, Cipher::AESCipher::CTRMode, 128) \ E(aes_128_gcm, cipher, Cipher::AESCipher::GCMMode, 128) \ E(aes_256_cbc, cipher, Cipher::AESCipher::CBCMode, 256) \ E(aes_256_ctr, cipher, Cipher::AESCipher::CTRMode, 256) \ E(chacha20_128, cipher, Cipher::ChaCha20, 128, 96) \ E(chacha20_256, cipher, Cipher::ChaCha20, 256, 96) struct Timings { u64 total_us { 0 }; u64 min_us { NumericLimits::max() }; u64 max_us { 0 }; size_t count { 0 }; size_t unit_bytes { 0 }; }; static HashMap> g_all_timings; static auto g_time_slice_per_size = Duration::from_seconds(3); constexpr size_t sizes_in_bytes[] = { 16, 1 * KiB, 16 * KiB, 256 * KiB, 1 * MiB, 16 * MiB }; static void run_benchmark_with_all_sizes(StringView name, Function func) { for (auto size : sizes_in_bytes) { auto result = ByteBuffer::create_uninitialized(size); if (result.is_error()) { warnln("Failed to allocate buffer of size {}", size); continue; } auto buffer = result.release_value(); fill_with_random(buffer); Timings timing_result { .unit_bytes = size }; warn("Running benchmark for {} with size {} for ~{}ms...", name, size, g_time_slice_per_size.to_milliseconds()); auto total_timer = Core::ElapsedTimer::start_new(Core::TimerType::Precise); for (; total_timer.elapsed_time() < g_time_slice_per_size;) { auto timer = Core::ElapsedTimer::start_new(Core::TimerType::Precise); func(buffer); auto elapsed = timer.elapsed_time(); timing_result.max_us = max(timing_result.max_us, elapsed.to_microseconds()); timing_result.min_us = min(timing_result.min_us, elapsed.to_microseconds()); timing_result.total_us += elapsed.to_microseconds(); timing_result.count++; } g_all_timings.ensure(name).set(size, timing_result); warnln("{}ms, {} ops, {}/s", total_timer.elapsed_milliseconds(), timing_result.count, human_readable_quantity(timing_result.unit_bytes * timing_result.count / timing_result.total_us * 1'000'000)); } } template static ErrorOr run_hash_benchmark(StringView name) { run_benchmark_with_all_sizes(name, [](auto& buffer) { auto digest = Algorithm::hash(buffer); AK::taint_for_optimizer(digest); }); return {}; } template static ErrorOr run_checksum_benchmark(StringView name) { run_benchmark_with_all_sizes(name, [](auto& buffer) { Algorithm checksum; checksum.update(buffer); auto digest = checksum.digest(); AK::taint_for_optimizer(digest); }); return {}; } template static ErrorOr run_auth_benchmark(StringView name) { auto key = TRY(ByteBuffer::create_uninitialized(128)); fill_with_random(key); run_benchmark_with_all_sizes(name, [&](auto& buffer) { Algorithm auth(key.bytes()); if constexpr (requires { auth.process(buffer.bytes()); }) { auto tag = auth.process(buffer.bytes()); AK::taint_for_optimizer(tag); } else if constexpr (IsSame) { auto tag = auth.process(buffer.bytes(), buffer.bytes()); AK::taint_for_optimizer(tag); } else { auth.update(buffer.bytes()); auto digest = auth.digest(); AK::taint_for_optimizer(digest); } }); return {}; } template static ErrorOr run_cipher_benchmark(StringView name, size_t key_bits, Options... options) { auto key = TRY(ByteBuffer::create_uninitialized(key_bits / 8)); fill_with_random(key); auto out_buffer = TRY(ByteBuffer::create_uninitialized(16 * MiB)); auto iv = TRY(ByteBuffer::create_uninitialized(key_bits / 8)); fill_with_random(iv); auto remaining_options = Tuple { options... }; ByteBuffer nonce; constexpr auto needs_nonce = remaining_options.size() > 0; if constexpr (needs_nonce) { nonce = TRY(ByteBuffer::create_uninitialized(remaining_options.template get<0>())); fill_with_random(nonce); } run_benchmark_with_all_sizes(name, [&](auto& buffer) { Optional cipher; if constexpr (needs_nonce) cipher = Algorithm(key.bytes(), nonce); else cipher = Algorithm(key.bytes(), key_bits, Crypto::Cipher::Intent::Encryption); auto out_bytes = out_buffer.bytes(); if constexpr (requires { cipher->encrypt(buffer, out_bytes, iv.bytes()); }) cipher->encrypt(buffer, out_bytes, iv.bytes()); else cipher->encrypt(buffer, out_bytes); AK::taint_for_optimizer(out_buffer); }); return {}; } static ErrorOr benchmark(StringView algorithm) { #define BENCH(name, type, algo, ...) \ if (algorithm == #name) { \ outln("Benchmarking {}...", #name); \ return run_##type##_benchmark(#name##sv __VA_OPT__(, ) __VA_ARGS__); \ } ALL_ALGORITHMS(BENCH); #undef BENCH return Error::from_string_literal("Unknown algorithm"); } static void print_benchmark_results() { // algo, size, min, max, avg, throughput outln("{:<20} {:<10} {:<10} {:<10} {:<10} {:<10}", "Algorithm", "Size", "Min us/op", "Max us/op", "Avg us/op", "Throughput"); for (auto& [algo, timings] : g_all_timings) { for (auto size : sizes_in_bytes) { auto t = timings.get(size); if (!t.has_value()) continue; auto& timing = t.value(); outln("{:<20} {:<10} {:<10} {:<10} {:<10} {:<10}/s", algo, human_readable_size(timing.unit_bytes), timing.min_us, timing.max_us, timing.total_us / timing.count, human_readable_quantity(timing.unit_bytes * timing.count / timing.total_us * 1'000'000)); } } } ErrorOr serenity_main(Main::Arguments arguments) { Vector algorithms; Optional time_slice; Core::ArgsParser args_parser; args_parser.add_positional_argument(algorithms, "Algorithms (or categories) to benchmark", "algorithms", Core::ArgsParser::Required::Yes); args_parser.add_option(time_slice, "Time slice for each benchmark size in milliseconds", "time-slice", 't', "time-slice"); args_parser.add_option(Core::ArgsParser::Option { .argument_mode = Core::ArgsParser::OptionArgumentMode::None, .help_string = "List all available algorithms", .long_name = "list", .short_name = 'l', .accept_value = [](auto) -> bool { warnln("{:<20} {:<10}", "Algorithm", "Type"); #define BENCH(name, type, ...) \ warnln("{:<20} {:<10}", #name, #type); ALL_ALGORITHMS(BENCH); #undef BENCH exit(0); }, }); args_parser.set_general_help("Benchmark LibCrypto's implementations of various cryptographic algorithms"); args_parser.parse(arguments); if (time_slice.has_value()) { if (auto slice = time_slice->to_number(); slice.has_value()) g_time_slice_per_size = Duration::from_milliseconds(*slice); else return Error::from_string_literal("Invalid time slice value"); } for (auto const& algorithm : algorithms) { auto found = false; #define BENCH(name, type, ...) \ if (algorithm == #type) { \ TRY(benchmark(#name##sv)); \ found = true; \ } ALL_ALGORITHMS(BENCH); #undef BENCH if (!found) TRY(benchmark(algorithm)); } print_benchmark_results(); return 0; }