diff --git a/src/cli/perf_hash_engine.cpp b/src/cli/perf_hash_engine.cpp new file mode 100644 index 00000000000..490591b061d --- /dev/null +++ b/src/cli/perf_hash_engine.cpp @@ -0,0 +1,76 @@ +/* +* (C) 2026 Jack Lloyd +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#include "perf.h" + +#if defined(BOTAN_HAS_HASH_ENGINES) + + #include + #include + #include + #include + #include + +namespace Botan_CLI { + +namespace { + +class PerfTest_HashEngine final : public PerfTest { + public: + void go(const PerfConfig& config) override { + const std::vector hashes = {"SHA-256", "SHA-512", "SHAKE-256(256)"}; + + const std::vector msg_sizes = {64, 256, 1024, 16384}; + const std::vector counts = {16, 32, 128}; + + for(const auto& hash_fn : hashes) { + std::unique_ptr engine; + + try { + engine = Botan::Hash_Engine::create_or_throw(hash_fn); + } catch(Botan::Not_Implemented&) { + continue; + } + + const size_t out_len = engine->output_length(); + + for(size_t msg_size : msg_sizes) { + for(size_t count : counts) { + std::vector input_buf(count * msg_size); + std::vector output_buf(count * out_len); + + config.rng().randomize(input_buf); + + std::vector> input_spans(count); + std::vector> output_spans(count); + + for(size_t i = 0; i < count; ++i) { + input_spans[i] = std::span(input_buf.data() + i * msg_size, msg_size); + output_spans[i] = std::span(output_buf.data() + i * out_len, out_len); + } + + const std::string name = + Botan::fmt("Hash_Engine({}) n={}", hash_fn, count); + + const uint64_t total_bytes = static_cast(count) * msg_size; + auto timer = config.make_timer(name, total_bytes, "batch_hash", engine->provider(), msg_size); + + timer->run_until_elapsed(config.runtime(), [&]() { engine->batch_hash(output_spans, input_spans); }); + + config.record_result(*timer); + } + } + } + } +}; + +} // namespace + +BOTAN_REGISTER_PERF_TEST("hash_engine", PerfTest_HashEngine); + +} // namespace Botan_CLI + +#endif diff --git a/src/lib/hash_engine/hash_engine.cpp b/src/lib/hash_engine/hash_engine.cpp new file mode 100644 index 00000000000..196f690be31 --- /dev/null +++ b/src/lib/hash_engine/hash_engine.cpp @@ -0,0 +1,155 @@ +/* +* (C) 2026 Jack Lloyd +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#include + +#include +#include + +#if defined(BOTAN_HAS_THREAD_UTILS) + #include +#endif + +namespace Botan { + +namespace { + +class Base_Hash_Engine final : public Hash_Engine { + public: + explicit Base_Hash_Engine(std::unique_ptr hash) : m_hash(std::move(hash)) {} + + std::string name() const override { return m_hash->name(); } + + std::string provider() const override { return "base"; } + + size_t output_length() const override { return m_hash->output_length(); } + + size_t parallelism() const override { return 1; } + + void batch_hash(std::span> outputs, std::span> inputs) override { + BOTAN_ARG_CHECK(outputs.size() == inputs.size(), + "Hash_Engine::batch_hash requires same number of inputs and outputs"); + + for(size_t i = 0; i != inputs.size(); ++i) { + BOTAN_ARG_CHECK(outputs[i].size() >= output_length(), "Hash_Engine::batch_hash output buffer too small"); + m_hash->update(inputs[i]); + m_hash->final(outputs[i].first(output_length())); + } + } + + private: + std::unique_ptr m_hash; +}; + +#if defined(BOTAN_HAS_THREAD_UTILS) + +class Threaded_Hash_Engine final : public Hash_Engine { + public: + Threaded_Hash_Engine(std::unique_ptr hash, Thread_Pool& threadpool) : + m_threadpool(threadpool), + m_name(hash->name()), + m_output_length(hash->output_length()), + m_thread_count(m_threadpool.worker_count()) { + BOTAN_ASSERT_NOMSG(m_thread_count > 0); + m_hashes.reserve(m_thread_count); + for(size_t i = 0; i != m_thread_count; ++i) { + m_hashes.push_back(hash->new_object()); + } + } + + std::string name() const override { return m_name; } + + std::string provider() const override { return "threads"; } + + size_t output_length() const override { return m_output_length; } + + size_t parallelism() const override { return m_thread_count; } + + void batch_hash(std::span> outputs, std::span> inputs) override { + BOTAN_ARG_CHECK(outputs.size() == inputs.size(), + "Hash_Engine::batch_hash requires same number of inputs and outputs"); + + const size_t count = inputs.size(); + const size_t threads = usable_threads(count); + + if(threads <= 1) { + hash_chunk(*m_hashes[0], m_output_length, outputs, inputs, 0, count); + return; + } + + dispatch(threads, count, [&](size_t t, size_t offset, size_t n) { + hash_chunk(*m_hashes[t], m_output_length, outputs, inputs, offset, n); + }); + } + + private: + size_t usable_threads(size_t count) const { + // Chunk work to not spread too thinly since just queuing the work + // in the pool has nonzero overhead. + constexpr size_t MIN_HASHES_PER_THREAD = 64; + + const size_t max_useful_threads = count / MIN_HASHES_PER_THREAD; + return std::min(m_thread_count, std::max(max_useful_threads, 1)); + } + + template + void dispatch(size_t threads, size_t count, F work_fn) { + const size_t per_thread = count / threads; + const size_t remainder = count % threads; + + std::vector> futures; + futures.reserve(threads); + + size_t offset = 0; + for(size_t t = 0; t != threads; ++t) { + // First remainder threads get 1 extra hash over the main batch + const size_t n = per_thread + (t < remainder ? 1 : 0); + futures.push_back(m_threadpool.run(work_fn, t, offset, n)); + offset += n; + } + + for(auto& f : futures) { + f.get(); + } + } + + static void hash_chunk(HashFunction& hash, + size_t output_length, + std::span> outputs, + std::span> inputs, + size_t offset, + size_t count) { + for(size_t i = offset; i != offset + count; ++i) { + hash.update(inputs[i]); + hash.final(outputs[i].first(output_length)); + } + } + + Thread_Pool& m_threadpool; + std::string m_name; + size_t m_output_length; + size_t m_thread_count; + std::vector> m_hashes; +}; + +#endif + +} // namespace + +std::unique_ptr Hash_Engine::create_or_throw(std::string_view hash_fn) { + auto hash = HashFunction::create_or_throw(hash_fn); + +#if defined(BOTAN_HAS_THREAD_UTILS) + auto& threadpool = Thread_Pool::global_instance(); + if(threadpool.worker_count() > 0) { + return std::make_unique(std::move(hash), threadpool); + } +#endif + + return std::make_unique(std::move(hash)); +} + +} // namespace Botan diff --git a/src/lib/hash_engine/hash_engine.h b/src/lib/hash_engine/hash_engine.h new file mode 100644 index 00000000000..4e7572f8320 --- /dev/null +++ b/src/lib/hash_engine/hash_engine.h @@ -0,0 +1,65 @@ +/* +* (C) 2026 Jack Lloyd +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#ifndef BOTAN_HASH_ENGINE_H_ +#define BOTAN_HASH_ENGINE_H_ + +#include +#include +#include +#include + +namespace Botan { + +class BOTAN_TEST_API Hash_Engine { + public: + /** + * @return name of the hash + */ + virtual std::string name() const = 0; + + /** + * @return provider of the engine (eg "base", "avx2", "avx512") + */ + virtual std::string provider() const = 0; + + /** + * @return output length of the hash function + */ + virtual size_t output_length() const = 0; + + /** + * @return native parallelism of this implementation + */ + virtual size_t parallelism() const = 0; + + /** + * Hash many inputs + * + * Each message must be exactly identical length + */ + virtual void batch_hash(std::span> outputs, std::span> inputs) = 0; + + /** + * Create a new Hash_Engine or throw Not_Implemented + */ + static std::unique_ptr create_or_throw(std::string_view hash_fn); + + Hash_Engine(const Hash_Engine& other) = delete; + Hash_Engine(Hash_Engine&& other) = delete; + + Hash_Engine& operator=(const Hash_Engine& other) = delete; + Hash_Engine& operator=(Hash_Engine&& other) = delete; + + virtual ~Hash_Engine() = default; + + protected: + Hash_Engine() = default; +}; + +} // namespace Botan + +#endif diff --git a/src/lib/hash_engine/info.txt b/src/lib/hash_engine/info.txt new file mode 100644 index 00000000000..7743bc26ed4 --- /dev/null +++ b/src/lib/hash_engine/info.txt @@ -0,0 +1,16 @@ + +HASH_ENGINES -> 20260222 + + + +name -> "Hash Engine" + + + +hash + + + +hash_engine.h + + diff --git a/src/tests/test_hash_engine.cpp b/src/tests/test_hash_engine.cpp new file mode 100644 index 00000000000..7e7f92aea64 --- /dev/null +++ b/src/tests/test_hash_engine.cpp @@ -0,0 +1,109 @@ +/* +* (C) 2026 Jack Lloyd +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#include "tests.h" + +#if defined(BOTAN_HAS_HASH_ENGINES) && defined(BOTAN_HAS_HASH) + + #include + #include + #include + #include + #include + + #include + +namespace Botan_Tests { + +namespace { + +class Hash_Engine_Tests final : public Test { + public: + std::vector run() override { + std::vector results; + + // Message sizes chosen to hit various padding edge cases + const std::vector msg_sizes = {0, 1, 7, 24, 32, 55, 56, 63, 64, 65, 111, 112, + 127, 128, 129, 135, 136, 137, 200, 255, 256, 257, 271, 272}; + + for(const auto& hash_fn : hash_engine_algorithms()) { + Test::Result result("Hash_Engine " + hash_fn); + + result.start_timer(); + + auto engine = Botan::Hash_Engine::create_or_throw(hash_fn); + auto ref_hash = Botan::HashFunction::create_or_throw(hash_fn); + + const size_t parallelism = engine->parallelism(); + const size_t output_length = engine->output_length(); + + result.test_str_eq("name", engine->name(), hash_fn); + result.test_str_not_empty("provider", engine->provider()); + result.test_sz_eq("output_length", output_length, ref_hash->output_length()); + result.test_sz_gte("parallelism >= 1", parallelism, 1); + + const size_t max_count = parallelism * 4; + + for(size_t count = 0; count != max_count; ++count) { + std::vector> input_bufs(count); + std::vector> output_bufs(count); + std::vector> input_spans(count); + std::vector> output_spans(count); + + for(size_t i = 0; i < count; ++i) { + output_bufs[i].resize(output_length); + output_spans[i] = output_bufs[i]; + } + + for(size_t msg_len : msg_sizes) { + for(size_t i = 0; i < count; ++i) { + input_bufs[i].resize(msg_len); + rng().randomize(input_bufs[i]); + input_spans[i] = input_bufs[i]; + } + + engine->batch_hash(output_spans, input_spans); + + for(size_t i = 0; i < count; ++i) { + auto expected = ref_hash->process>(input_spans[i]); + result.test_bin_eq(hash_fn, output_bufs[i], expected); + } + } + } + + result.end_timer(); + results.push_back(result); + } + + return results; + } + + private: + static std::vector hash_engine_algorithms() { + return std::vector { + #if defined(BOTAN_HAS_SHA2_32) + "SHA-256", + #endif + #if defined(BOTAN_HAS_SHA2_64) + "SHA-512", + #endif + #if defined(BOTAN_HAS_SHAKE) + "SHAKE-256(192)", "SHAKE-256(256)", + #endif + #if defined(BOTAN_HAS_SM3) + "SM3", + #endif + }; + } +}; + +BOTAN_REGISTER_TEST("hash", "hash_engine", Hash_Engine_Tests); + +} // namespace + +} // namespace Botan_Tests + +#endif