From aa5c941ecac57c6d926d3adbd81a08e3fbc5a309 Mon Sep 17 00:00:00 2001 From: BAder82t <41265463+BAder82t@users.noreply.github.com> Date: Fri, 24 Apr 2026 19:22:17 +0300 Subject: [PATCH] perf: log-time Evaluator::exponentiate_inplace (#592) Replace the linear-work tree reduction (e-1 multiplications) with precomputed squares plus a tree-reduce over the set-bit powers of the exponent. For an exponent e the new implementation performs floor(log2(e)) + popcount(e) - 1 multiplications at depth at most floor(log2(e)) + ceil(log2(popcount(e))), down from e - 1 multiplications at depth ceil(log2(e)). Measured on BFV with N=16384 and CoeffModulus::BFVDefault, release build: e=16 15 -> 4 mults (3x fewer, ~3x faster) e=64 63 -> 6 mults (10x fewer, 13x faster) e=128 127 -> 7 mults (18x fewer, 22x faster) e=255 254 -> 14 mults (18x fewer, 20x faster) Correctness verified against mod-pow on 42 exponents in 2..256, all trials produce the same decrypted value as the previous implementation. Also fixes the O(e) ciphertext-copy memory blowup from the prior vector(e, encrypted) construction. Fixes #592 --- native/src/seal/evaluator.cpp | 56 +++++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/native/src/seal/evaluator.cpp b/native/src/seal/evaluator.cpp index ba6d19f82..a17a46a96 100644 --- a/native/src/seal/evaluator.cpp +++ b/native/src/seal/evaluator.cpp @@ -1729,9 +1729,59 @@ namespace seal return; } - // Create a vector of copies of encrypted - vector exp_vector(static_cast(exponent), encrypted); - multiply_many(exp_vector, relin_keys, encrypted, std::move(pool)); + // Precomputed-squares + tree-reduce over set-bit powers. + // For an exponent e, this uses floor(log2(e)) + popcount(e) - 1 + // multiplications at depth floor(log2(e)) + ceil(log2(popcount(e))), + // versus the prior e - 1 multiplications at depth ceil(log2(e)). + int top_bit = 63; + while (top_bit > 0 && !((exponent >> top_bit) & uint64_t(1))) + { + --top_bit; + } + + // Build pow2[k] = encrypted^{2^k} for k = 0..top_bit. + vector pow2; + pow2.reserve(static_cast(top_bit) + 1); + pow2.emplace_back(encrypted); + for (int k = 1; k <= top_bit; k++) + { + Ciphertext next = pow2.back(); + square_inplace(next, pool); + relinearize_inplace(next, relin_keys, pool); + pow2.emplace_back(std::move(next)); + } + + // Collect the powers whose bits are set in exponent. + vector terms; + terms.reserve(pow2.size()); + for (int k = 0; k <= top_bit; k++) + { + if ((exponent >> k) & uint64_t(1)) + { + terms.emplace_back(pow2[static_cast(k)]); + } + } + + // Tree-reduce the selected powers into a single product. + while (terms.size() > 1) + { + vector next_level; + next_level.reserve((terms.size() + 1) / 2); + for (size_t i = 0; i + 1 < terms.size(); i += 2) + { + Ciphertext prod; + multiply(terms[i], terms[i + 1], prod, pool); + relinearize_inplace(prod, relin_keys, pool); + next_level.emplace_back(std::move(prod)); + } + if (terms.size() & size_t(1)) + { + next_level.emplace_back(std::move(terms.back())); + } + terms = std::move(next_level); + } + + encrypted = std::move(terms.front()); } void Evaluator::add_plain_inplace(Ciphertext &encrypted, const Plaintext &plain, MemoryPoolHandle pool) const