/// @file dpf/eval_sequence.hpp /// @brief Evaluate a sorted list of DPF inputs. /// @details The range is nondecreasing; an unsorted range throws /// `std::runtime_error`. `return_output_only_tag_` stores one share /// per point. `return_entire_node_tag_` stores whole leaves and is /// the default. A `sequence_recipe` repeats the list, and a sequence /// memoizer bound to that recipe object resumes the traversal. /// @snippet evaluation/eval_sequence.cpp eval-sequence /// @author Ryan Henry /// @author Christopher Jiang /// @copyright Copyright (c) 2019-2024 Ryan Henry and [others](@ref authors) /// @license Released under a GNU General Public v2.0 (GPLv2) license; /// see [LICENSE.md](@ref license) for details. #ifndef LIBDPF_INCLUDE_DPF_EVAL_SEQUENCE_HPP__ #define LIBDPF_INCLUDE_DPF_EVAL_SEQUENCE_HPP__ #include #include "hedley/hedley.h" #include #include #include #include #include #include #include #include #include #include "dpf/dpf_key.hpp" #include "dpf/eval_common.hpp" #include "dpf/eval_target.hpp" #include "dpf/eval_point.hpp" #include "dpf/path_memoizer.hpp" #include "dpf/sequence_memoizer.hpp" #include "dpf/sequence_utils.hpp" #include "dpf/subsequence_iterable.hpp" #include "dpf/subinterval_iterable.hpp" namespace dpf { namespace internal { template auto eval_sequence_entire_node(const DpfKey & dpf, ForwardIterator begin, ForwardIterator end, OutputBuffers && outbufs, std::index_sequence) { static constexpr std::size_t outputs_per_leaf = DpfKey::outputs_per_leaf; auto path = make_basic_path_memoizer(dpf); std::size_t i = 0; // DPF_UNROLL_LOOP for (auto it = begin; it != end; ++it, ++i) { if constexpr(utils::is_packed_subbyte_v>) { auto nodes = std::make_tuple(dpf::eval_point(dpf, *it, path).node...); (store_leaf_bytes(utils::get(outbufs), i, std::get(nodes)), ...); } else { auto temp = std::make_tuple(dpf::eval_point(dpf, *it, path).node...); (std::memcpy(&utils::get(outbufs)[i*outputs_per_leaf], &utils::get(temp), sizeof(typename DpfKey::concrete_output_type)*outputs_per_leaf), ...); } } return utils::make_tuple( dpf::subsequence_iterable(outbufs))), ForwardIterator>(std::begin(utils::get(outbufs)), begin, end)...); } template HEDLEY_ALWAYS_INLINE void assign_eval_slot(Slot && slot, Val && val) { using val_t = std::decay_t; if constexpr (is_secret_share_v>) { using elem_t = std::decay_t; if constexpr (is_secret_share_v) slot = elem_t::from_raw(val.raw()); else slot = elem_t::from_raw(static_cast(val)); } else if constexpr (is_secret_share_v) { slot = val.raw(); } else { slot = std::forward(val); } } template auto eval_sequence_output_only(const DpfKey & dpf, ForwardIterator begin, ForwardIterator end, OutputBuffers && outbufs, std::index_sequence) { auto path = make_basic_path_memoizer(dpf); std::size_t i = 0; // DPF_UNROLL_LOOP for (auto it = begin; it != end; ++it, ++i) { (assign_eval_slot(utils::get(outbufs)[i], *dpf::eval_point(dpf, *it, path)), ...); } if (i == 0) { return utils::make_tuple(subinterval_iterable(std::begin(utils::get(outbufs)), utils::size(utils::get(outbufs)), 0, 0, 0, 0, false)...); } return utils::make_tuple(subinterval_iterable(std::begin(utils::get(outbufs)), utils::size(utils::get(outbufs)), 0, i-1, 0, 0)...); } } // namespace internal template && !is_multilevel_key_v, bool> = true, std::enable_if_t, bool> = true, std::enable_if_t, bool> = true> inline auto eval_sequence(const DpfKey & dpf, ForwardIterator begin, ForwardIterator end, OutputBuffers && outbufs, ReturnType return_type = ReturnType{}) { static_assert(std::is_same_v || std::is_same_v); if constexpr(std::is_same_v) { return internal::eval_sequence_entire_node(dpf, begin, end, outbufs, std::make_index_sequence<1+sizeof...(Is)>{}); } else { return internal::eval_sequence_output_only(dpf, begin, end, outbufs, std::make_index_sequence<1+sizeof...(Is)>{}); } } /// Evaluate the sorted range `[begin, end)`, allocating a buffer. /// @param return_type `return_entire_node_tag_{}` or `return_output_only_tag_{}`. /// @return Pair of buffer (or tuple of buffers) and an iterable in list order. template && !is_multilevel_key_v, bool> = true, std::enable_if_t, bool> = true> auto eval_sequence(const DpfKey & dpf, ForwardIterator begin, ForwardIterator end, ReturnType return_type = ReturnType{}) { auto outbufs = utils::make_tuple( make_output_buffer_for_subsequence(dpf, begin, end, return_type), make_output_buffer_for_subsequence(dpf, begin, end, return_type)...); // moving `outbufs` is allowed as the `outbufs` are `std::vectors` // the underlying data remains on the heap // and thus the data the iterable refers to is still valid auto iterable = eval_sequence(dpf, begin, end, outbufs, return_type); return std::make_pair(std::move(outbufs), std::move(iterable)); } template inline auto eval_sequence_breadth_first(const DpfKey & dpf, ForwardIterator begin, ForwardIterator end, OutputBuffer && outbuf) { assert_not_wildcard_output(dpf); using dpf_type = DpfKey; using input_type = typename DpfKey::input_type; using node_type = typename DpfKey::interior_node; using output_type = typename DpfKey::concrete_output_type; HEDLEY_PRAGMA(GCC diagnostic push) HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes") using allocator = aligned_allocator; using unique_ptr = typename allocator::unique_ptr; HEDLEY_PRAGMA(GCC diagnostic pop) allocator alloc = allocator{}; if (HEDLEY_UNLIKELY(!std::is_sorted(begin, end))) { throw std::runtime_error("list must be sorted"); } if (begin == end) { return subsequence_iterable( std::begin(outbuf), begin, end); } auto mask = dpf_type::msb_mask; std::size_t nodes_in_sequence = std::distance(begin, end); unique_ptr memo{alloc.allocate_unique_ptr(nodes_in_sequence*2)}; bool curhalf = (dpf_type::depth ^ 1) & 1; memo[!curhalf*nodes_in_sequence + 0] = dpf.root(); std::list splits{begin, end}; std::size_t level_index = 1; auto func = [&](const bool flip = false) { std::size_t i = 0, j = 0; const node_type cw[2] = { dpf.correction_word(level_index-1, 0), dpf.correction_word(level_index-1, 1) }; // `lower` and `upper` are always adjacent elements of `splits` with `lower` < `upper` // [lower, upper) = "block" for (auto upper = std::begin(splits), lower = upper++; upper != std::end(splits); lower = upper++) { // `upper_bound()` returns iterator to first element where the relevant bit (based on `mask`) is set auto it = std::upper_bound(*lower, *upper, mask, [&flip](auto a, auto b){ return static_cast(a&b) ^ flip; }); if (it == *lower) // right only since first element in "block" requires right traversal { memo[curhalf*nodes_in_sequence + i++] = dpf_type::traverse_interior(memo[!curhalf*nodes_in_sequence + j++], cw[1], 1); } else if (it == *upper) // left only since no element in "block" requires right traversal { memo[curhalf*nodes_in_sequence + i++] = dpf_type::traverse_interior(memo[!curhalf*nodes_in_sequence + j++], cw[0], 0); } else // both ways since some (non-lower) element within "block" requires right traversal { auto cur_node = memo[!curhalf*nodes_in_sequence + j++]; auto kids = dpf_type::traverse_interior01(cur_node, cw[0], cw[1]); memo[curhalf*nodes_in_sequence + i++] = kids[0]; memo[curhalf*nodes_in_sequence + i++] = kids[1]; splits.insert(upper, it); } } }; if (dpf_type::depth >= level_index) { func(utils::uses_signed_msb_v); ++level_index; mask >>= 1; curhalf =! curhalf; } for (; level_index <= dpf_type::depth; ++level_index, mask>>=1, curhalf=!curhalf) { func(); } HEDLEY_PRAGMA(GCC diagnostic push) HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes") auto cw = dpf.template leaf(); auto buf = memo.get(); constexpr auto clz = utils::countl_zero_symmetric_difference{}; auto curr = begin, prev = curr; for (std::size_t i = 0, j = 0; i < nodes_in_sequence; ++i) { j += (clz(*prev, *curr)) < dpf_type::depth; auto leaf = dpf_type::template traverse_exterior(buf[j], get_if_lo_bit(cw, buf[j])); if constexpr (utils::is_packed_subbyte_v) { store_leaf_bytes(outbuf, i, leaf); } else { std::memcpy(&outbuf[i*dpf_type::outputs_per_leaf], &leaf, sizeof(output_type)*dpf_type::outputs_per_leaf); } prev = curr++; } HEDLEY_PRAGMA(GCC diagnostic pop) return subsequence_iterable(std::begin(outbuf), begin, end); } template auto eval_sequence_breadth_first(const DpfKey & dpf, ForwardIterator begin, ForwardIterator end) { auto outbuf = make_output_buffer_for_subsequence(dpf, begin, end); // moving `outbuf` is allowed as `outbuf` is a `std::vectors` // the underlying data remains on the heap // and thus the data the iterable refers to is still valid auto iterable = eval_sequence_breadth_first(dpf, begin, end, outbuf); return std::make_pair(std::move(outbuf), std::move(iterable)); } namespace internal { template inline auto eval_sequence_interior(const DpfKey & dpf, const sequence_recipe & recipe, SequenceMemoizer && memoizer, std::size_t to_level = DpfKey::depth) { using dpf_type = DpfKey; using node_type = typename DpfKey::interior_node; // level_index represents the current level being built // level_index = 0 => root // level_index = depth => last layer of interior nodes if (recipe.num_leaf_nodes() == 0) return; std::size_t level_index = memoizer.assign_dpf(dpf, recipe); std::size_t recipe_index = recipe.level_endpoints()[level_index-1]; std::size_t nodes_at_level = memoizer.get_nodes_at_level(level_index-1); for (; level_index <= to_level; level_index = memoizer.advance_level(), nodes_at_level = memoizer.get_nodes_at_level(level_index-1)) { const node_type cw[2] = { dpf.correction_word(level_index-1, 0), dpf.correction_word(level_index-1, 1) }; auto prevbuf = memoizer[level_index-1]; auto currbuf = memoizer[level_index]; DPF_UNROLL_LOOP for (std::size_t input_index = 0, output_index = 0; input_index < nodes_at_level; ++input_index, ++recipe_index) { if (memoizer.traverse_first(recipe_index) == true) { bool dir = memoizer.get_direction(0); currbuf[output_index++] = dpf_type::traverse_interior(prevbuf[input_index], cw[dir], dir); } if (memoizer.traverse_second(recipe_index) == true) { bool dir = memoizer.get_direction(1); currbuf[output_index++] = dpf_type::traverse_interior(prevbuf[input_index], cw[dir], dir); } } } } template inline auto eval_sequence_exterior_entire_node(const DpfKey & dpf, const sequence_recipe & recipe, OutputBuffer && outbuf, SequenceMemoizer && memoizer) { assert_not_wildcard_output(dpf); using dpf_type = DpfKey; using output_type = typename DpfKey::concrete_output_type; auto nodes_in_interval = recipe.num_leaf_nodes(); HEDLEY_PRAGMA(GCC diagnostic push) HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes") auto buf = memoizer[dpf.depth]; DPF_UNROLL_LOOP for (std::size_t j = 0; j < nodes_in_interval; ++j) { auto leaf = dpf.template traverse_exterior(buf[j]); if constexpr (utils::is_packed_subbyte_v) { store_leaf_bytes(outbuf, j, leaf); } else { std::memcpy(&outbuf[j*dpf_type::outputs_per_leaf], &leaf, sizeof(output_type)*dpf_type::outputs_per_leaf); } } HEDLEY_PRAGMA(GCC diagnostic pop) } template inline auto eval_sequence_exterior_output_only(const DpfKey & dpf, const sequence_recipe & recipe, OutputBuffer && outbuf, SequenceMemoizer && memoizer) { assert_not_wildcard_output(dpf); using dpf_type = DpfKey; using output_type = typename DpfKey::concrete_output_type; HEDLEY_PRAGMA(GCC diagnostic push) HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes") auto cw = dpf.template leaf(); using node_type = typename DpfKey::exterior_node; using leaf_node_type = std::tuple_element_t; auto buf = memoizer[dpf.depth]; leaf_node_type node; // DPF_UNROLL_LOOP for (std::size_t i = 0, j = -1, prev = -1, curr; i < recipe.output_indices().size(); prev = curr, ++i) { curr = recipe.output_indices()[i]/dpf_type::outputs_per_leaf; if (prev != curr) { ++j; node = dpf_type::template traverse_exterior(buf[j], get_if_lo_bit(cw, buf[j])); } auto v = extract_leaf(node, recipe.output_indices()[i] % dpf_type::outputs_per_leaf); using elem_t = std::decay_t; if constexpr (is_secret_share_v) outbuf[i] = elem_t::from_raw(v); else outbuf[i] = v; } HEDLEY_PRAGMA(GCC diagnostic pop) } template , bool> = true, std::size_t ...IIs> auto eval_sequence(const DpfKey & dpf, const sequence_recipe & recipe, OutputBuffers && outbufs, SequenceMemoizer && memoizer, ReturnType return_type, std::index_sequence) { internal::eval_sequence_interior(dpf, recipe, memoizer); static_assert(std::is_same_v || std::is_same_v); if constexpr (std::is_same_v) { (internal::eval_sequence_exterior_entire_node(dpf, recipe, utils::get(outbufs), memoizer), ...); return utils::make_tuple( recipe_subsequence_iterable(std::begin(utils::get(outbufs)), recipe.output_indices())...); } else { (internal::eval_sequence_exterior_output_only(dpf, recipe, utils::get(outbufs), memoizer), ...); const auto nout = recipe.output_indices().size(); if (nout == 0) { return utils::make_tuple(subinterval_iterable(std::begin(utils::get(outbufs)), utils::size(utils::get(outbufs)), 0, 0, 0, 0, false)...); } return utils::make_tuple(subinterval_iterable(std::begin(utils::get(outbufs)), utils::size(utils::get(outbufs)), 0, nout-1, 0, 0)...); } } } // namespace internal /// Evaluate `recipe` into a named buffer, reusing `memoizer`. /// @param recipe The same object `memoizer` was constructed from. /// @param outbufs Named buffer. The returned iterable refers into it. template , bool> = true, std::enable_if_t, bool> = true> HEDLEY_ALWAYS_INLINE auto eval_sequence(const DpfKey & dpf, const sequence_recipe & recipe, OutputBuffers & outbufs, SequenceMemoizer && memoizer, // NOLINT(runtime/references) ReturnType return_type = ReturnType{}) { assert_not_wildcard_output(dpf); assert_not_wildcard_input(dpf); return internal::eval_sequence(dpf, recipe, outbufs, memoizer, return_type, std::make_index_sequence<1+sizeof...(Is)>()); } template >, bool> = true, std::enable_if_t, bool> = true> HEDLEY_ALWAYS_INLINE auto eval_sequence(const DpfKey & dpf, const sequence_recipe & recipe, OutputBuffers & outbufs, ReturnType return_type = ReturnType{}) // NOLINT(runtime/references) { return eval_sequence(dpf, recipe, outbufs, dpf::make_double_space_sequence_memoizer(recipe), return_type); } template >, bool> = true, std::enable_if_t, bool> = true> HEDLEY_ALWAYS_INLINE auto eval_sequence(const DpfKey & dpf, const sequence_recipe & recipe, SequenceMemoizer && memoizer, ReturnType return_type = ReturnType{}) { auto outbufs = utils::make_tuple( make_output_buffer_for_recipe_subsequence(dpf, recipe, return_type), make_output_buffer_for_recipe_subsequence(dpf, recipe, return_type)...); // moving `outbufs` is allowed as the `outbufs` are `std::vectors` // the underlying data remains on the heap // and thus the data the iterable refers to is still valid auto iterable = eval_sequence(dpf, recipe, outbufs, memoizer, return_type); return std::make_pair(std::move(outbufs), std::move(iterable)); } template , bool> = true> HEDLEY_ALWAYS_INLINE auto eval_sequence(const DpfKey & dpf, const sequence_recipe & recipe, ReturnType return_type = ReturnType{}) { return eval_sequence(dpf, recipe, dpf::make_double_space_sequence_memoizer(recipe), return_type); } } // namespace dpf #endif // LIBDPF_INCLUDE_DPF_EVAL_SEQUENCE_HPP__