/// @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 #include #include "dpf/dpf_key.hpp" #include "dpf/eval_common.hpp" #include "dpf/eval_target.hpp" #include "dpf/eval_point.hpp" #include "dpf/eval_interval.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" #include "dpf/verifiable.hpp" namespace dpf { template void prove_sequence(const KeyT & key, ForwardIterator begin, ForwardIterator end, prove_ref pr); // Contiguous runs at least this long use the interval tree (one expand per // node). Shorter runs stay on the path memoizer. Isolated points at least // this many use the breadth-first block walk, which beats a fresh path per // point once the list is wide. inline constexpr std::size_t sequence_interval_run = 24; // Breadth-first shares prefixes on sparse lists, but its per-level block-split // bookkeeping loses to a path memoizer once single-child expands cut the // path PRG cost in half. Keep the explicit `eval_sequence_breadth_first` // entry point; do not auto-select it from `eval_sequence`. inline constexpr std::size_t sequence_breadth_points = std::numeric_limits::max(); template inline auto eval_sequence_breadth_first(const DpfKey & dpf, ForwardIterator begin, ForwardIterator end, OutputBuffer && outbuf); template void scatter_interval_run(const DpfKey & dpf, InputT from, InputT to, OutputBuffers & outbufs, std::size_t index0, std::index_sequence) { // std::make_tuple, not utils::make_tuple: one output must stay a tuple // so each selected buffer is addressable by index. auto ibufs = std::make_tuple( make_output_buffer_for_interval(from, to)...); (void)eval_interval(dpf, from, to, ibufs); constexpr auto opl = DpfKey::outputs_per_leaf; constexpr auto lg = DpfKey::lg_outputs_per_leaf; constexpr auto mod_pow_2 = utils::mod_pow_2{}; const std::size_t preclip = mod_pow_2(dpf.offset_x(from), lg); constexpr auto to_int = utils::to_integral_type{}; auto span = to_int(to) - to_int(from); constexpr auto bits = utils::bitlength_of_v; if constexpr (bits < utils::bitlength_of_v) span &= (decltype(span){1} << bits) - 1; const std::size_t n = static_cast(span) + 1; constexpr std::size_t leaf_mask = (std::size_t{1} << lg) - 1; auto one = [&](auto which) { constexpr std::size_t k = decltype(which)::value; auto & src = utils::get(ibufs); auto & dst = utils::get(outbufs); auto put = [](auto & slot, const auto & val) { assign_share_slot(slot, val); }; if constexpr (Entire) { for (std::size_t j = 0; j < n; ++j) { const std::size_t elem_i = preclip + j; const std::size_t leaf = elem_i - (elem_i & leaf_mask); for (std::size_t lane = 0; lane < opl; ++lane) put(dst[(index0 + j) * opl + lane], src[leaf + lane]); } } else { for (std::size_t j = 0; j < n; ++j) put(dst[index0 + j], src[preclip + j]); } }; (one(std::integral_constant{}), ...); } namespace internal { /// @brief Leaf interior nodes for a list, eight paths at a time. /// @details One scalar spine up to the shallowest divergence, then /// `tree::traverse8` across the whole suffix of each level. `walk[i]` /// is the offset input with the signed MSB already flipped, matching /// `eval_point`. template std::vector sequence_wide_leaves( const DpfKey & dpf, const typename DpfKey::input_type * walk, std::size_t n) { using node = typename DpfKey::interior_node; std::vector leaves(n); if (n == 0) return leaves; constexpr auto clz = utils::countl_zero_symmetric_difference< typename DpfKey::input_type>{}; std::size_t common = dpf.depth + 1; for (std::size_t i = 1; i < n; ++i) common = std::min(common, clz(walk[0], walk[i]) + 1); std::array path{}; path[0] = dpf.root(); auto mask = dpf.msb_mask; for (std::size_t level = 1; level < common && level <= dpf.depth; ++level, mask >>= 1) { const bool bit = (mask & walk[0]) != 0; path[level] = DpfKey::traverse_interior(path[level - 1], dpf.correction_word(level - 1, bit), bit, DpfKey::tree::is_last_level(level - 1, dpf.depth)); } if (common > dpf.depth) { std::fill(leaves.begin(), leaves.end(), path[dpf.depth]); return leaves; } std::vector cur(n, path[common - 1]); std::vector next(n); mask = dpf.msb_mask >> (common - 1); for (std::size_t level = common; level <= dpf.depth; ++level, mask >>= 1) { const node cw0 = dpf.correction_word(level - 1, false); const node cw1 = dpf.correction_word(level - 1, true); const bool last = DpfKey::tree::is_last_level(level - 1, dpf.depth); std::size_t i = 0; for (; i + 8 <= n; i += 8) { node parents[8]; node cws[8]; bool dirs[8]; for (int k = 0; k < 8; ++k) { dirs[k] = (mask & walk[i + static_cast(k)]) != 0; cws[k] = dirs[k] ? cw1 : cw0; parents[k] = cur[i + static_cast(k)]; } node outs[8]; DpfKey::tree::traverse8(parents, cws, dirs, outs); for (int k = 0; k < 8; ++k) next[i + static_cast(k)] = outs[k]; } for (; i < n; ++i) { const bool bit = (mask & walk[i]) != 0; next[i] = DpfKey::traverse_interior(cur[i], bit ? cw1 : cw0, bit, last); } cur.swap(next); } return cur; } 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; using input_type = typename DpfKey::input_type; constexpr bool any_packed = (utils::is_packed_subbyte_v> || ...); constexpr bool any_bit = (std::is_same_v, dpf::bit> || ...); constexpr bool integral_in = std::is_integral_v; // Bit and packed leaves are proxy buffers. The interval scatter writes // ordinary word slots; those outputs stay on the path memoizer. constexpr bool interval_ok = integral_in && !any_packed && !any_bit; auto write_point = [&](auto & path, std::size_t i, const input_type & x) { if constexpr (any_packed) { auto nodes = std::make_tuple(dpf::eval_point(dpf, x, path).node...); (store_leaf_bytes(utils::get(outbufs), i, std::get(nodes)), ...); } else { auto temp = std::make_tuple(dpf::eval_point(dpf, x, path).node...); (utils::raw_memcpy(&utils::get(outbufs)[i * outputs_per_leaf], &utils::get(temp), sizeof(typename DpfKey::concrete_output_type) * outputs_per_leaf), ...); } }; if constexpr (interval_ok) { using iter_cat = typename std::iterator_traits::iterator_category; bool classify = true; if constexpr (std::is_base_of_v) { // Below this length the interval-run cover does not apply, so skip // the classification pass. classify = static_cast(std::distance(begin, end)) >= sequence_interval_run; } if (classify) { bool sorted = true; std::size_t n = 0; std::size_t longest = 0; std::size_t cur = 0; bool dup = false; input_type prev{}; for (auto it = begin; it != end; ++it, ++n) { if (n == 0) { prev = *it; longest = 1; cur = 1; continue; } const input_type x = *it; if (x < prev) sorted = false; else if (x == prev) dup = true; if (x == static_cast(prev + input_type{1})) { ++cur; if (cur > longest) longest = cur; } else cur = 1; prev = x; } if (sorted && longest >= sequence_interval_run) { std::size_t i = 0; for (auto it = begin; it != end; ) { const input_type run_from = *it; input_type run_to = run_from; auto run_end = it; ++run_end; while (run_end != end && *run_end == static_cast(run_to + input_type{1})) { run_to = *run_end; ++run_end; } const auto span_n = static_cast(std::distance(it, run_end)); if (span_n >= sequence_interval_run) { scatter_interval_run(dpf, run_from, run_to, outbufs, i, std::index_sequence{}); i += span_n; } else { auto path = make_basic_path_memoizer(dpf); for (auto p = it; p != run_end; ++p, ++i) write_point(path, i, *p); } it = run_end; } return utils::make_tuple( dpf::subsequence_iterable(outbufs))), ForwardIterator>(std::begin(utils::get(outbufs)), begin, end)...); } // Breadth-first pays for a block split at every level. On a short // tree (8-bit inputs) that overhead loses to the path memoizer. // The one-output overload is the only one; keep it out of the // multi-output instantiation. if constexpr (sizeof...(Is) == 1) { if (sorted && !dup && n >= sequence_breadth_points && DpfKey::depth >= 16) { eval_sequence_breadth_first(dpf, begin, end, utils::get<0>(outbufs)); return utils::make_tuple( dpf::subsequence_iterable(outbufs))), ForwardIterator>(std::begin(utils::get(outbufs)), begin, end)...); } } } } const std::size_t nseq = static_cast(std::distance(begin, end)); if constexpr (sizeof...(Is) == 1 && !DpfKey::is_verifiable && prg_has_indep4::value) { if (nseq >= 16) { using input_type = typename DpfKey::input_type; std::vector walk(nseq); std::vector lane(nseq); std::size_t i = 0; for (auto it = begin; it != end; ++it, ++i) { lane[i] = dpf.offset_x(*it); walk[i] = lane[i]; utils::flip_msb_if_signed_integral(walk[i]); } const auto leaves = sequence_wide_leaves(dpf, walk.data(), nseq); constexpr std::size_t ids[] = {Is...}; constexpr std::size_t I0 = ids[0]; using output_type = typename DpfKey::concrete_output_type; auto & dst = utils::get<0>(outbufs); for (std::size_t p = 0; p < nseq; ++p) { auto wrapped = make_eval_dpf_output( dpf.template traverse_exterior(leaves[p]), lane[p]); utils::raw_memcpy(&dst[p * outputs_per_leaf], &wrapped.node, sizeof(output_type) * outputs_per_leaf); } return utils::make_tuple( dpf::subsequence_iterable(outbufs))), ForwardIterator>(std::begin(utils::get(outbufs)), begin, end)...); } } auto path = make_basic_path_memoizer(dpf); std::size_t i = 0; for (auto it = begin; it != end; ++it, ++i) write_point(path, i, *it); 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) { assign_share_slot(slot, std::forward(val)); } template auto eval_sequence_output_only(const DpfKey & dpf, ForwardIterator begin, ForwardIterator end, OutputBuffers && outbufs, std::index_sequence) { using input_type = typename DpfKey::input_type; constexpr bool any_packed = (utils::is_packed_subbyte_v> || ...); constexpr bool any_bit = (std::is_same_v, dpf::bit> || ...); constexpr bool integral_in = std::is_integral_v; auto write_point = [&](auto & path, std::size_t i, const input_type & x) { (assign_eval_slot(utils::get(outbufs)[i], *dpf::eval_point(dpf, x, path)), ...); }; auto finish = [&](std::size_t i) { 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)...); }; if constexpr (integral_in && !any_packed && !any_bit) { bool sorted = true; std::size_t longest = 0; std::size_t cur = 0; std::size_t n = 0; input_type prev{}; for (auto it = begin; it != end; ++it, ++n) { if (n == 0) { prev = *it; longest = 1; cur = 1; continue; } const input_type x = *it; if (x < prev) sorted = false; if (x == static_cast(prev + input_type{1})) { ++cur; if (cur > longest) longest = cur; } else cur = 1; prev = x; } if (sorted && longest >= sequence_interval_run) { auto path = make_basic_path_memoizer(dpf); std::size_t i = 0; for (auto it = begin; it != end; ) { const input_type run_from = *it; input_type run_to = run_from; auto run_end = it; ++run_end; while (run_end != end && *run_end == static_cast(run_to + input_type{1})) { run_to = *run_end; ++run_end; } const auto span_n = static_cast(std::distance(it, run_end)); if (span_n >= sequence_interval_run) { scatter_interval_run(dpf, run_from, run_to, outbufs, i, std::index_sequence{}); i += span_n; } else { for (auto p = it; p != run_end; ++p, ++i) write_point(path, i, *p); } it = run_end; } return finish(i); } } auto path = make_basic_path_memoizer(dpf); std::size_t i = 0; for (auto it = begin; it != end; ++it, ++i) write_point(path, i, *it); return finish(i); } } // namespace internal /// \complexity O(n k) interior traversals in the worst case and O(k) node workspace. k is the number of listed points and n is `depth`. The breadth-first buffer is 2k nodes, so each level traverses at most one node per point. Shared prefixes do fewer traversals. A recipe memoizer instead stores O(recipe leaf nodes) (see that memoizer). 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{}) { (void)return_type; 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)>{}); } } /// @brief Evaluate a sorted sequence and fold the same VDPF proof as `prove_sequence`. /// @details Uses interval-run covers (not a path-memo double walk). Values are /// written by a single subsequent `eval_sequence` without proof. /// \complexity O(n k) interior traversals in the worst case and O(k) node workspace. k is the number of listed points and n is `depth`. The breadth-first buffer is 2k nodes, so each level traverses at most one node per point. Shared prefixes do fewer traversals. A recipe memoizer instead stores O(recipe leaf nodes) (see that memoizer). 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, prove_ref pr, ReturnType return_type = ReturnType{}) { static_assert(DpfKey::is_verifiable, "eval_sequence(..., prove(π)): key must carry dpf::verifiable"); prove_sequence(dpf, begin, end, pr); return eval_sequence(dpf, begin, end, std::forward(outbufs), return_type); } /// @brief Evaluate a sorted sequence and fold each written output into a sketch. /// \complexity O(n k) interior traversals in the worst case and O(k) node workspace. k is the number of listed points and n is `depth`. The breadth-first buffer is 2k nodes, so each level traverses at most one node per point. Shared prefixes do fewer traversals. A recipe memoizer instead stores O(recipe leaf nodes) (see that memoizer). 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, sketch_ref & sk, ReturnType return_type = ReturnType{}) { static_assert(DpfKey::is_extractable, "eval_sequence(..., sketch(σ)): key must carry dpf::extractable"); auto ret = eval_sequence(dpf, begin, end, outbufs, return_type); if constexpr (sizeof...(Is) == 0) { for (std::size_t k = 0; k < utils::size(outbufs); ++k) sk.absorb(outbufs[k]); } return ret; } /// @brief Evaluate the sorted range `[begin, end)`, allocating a buffer. /// @tparam I output index /// @tparam Is is /// @tparam DpfKey DPF key type /// @tparam ForwardIterator forward iterator type /// @tparam ReturnType return type /// @tparam DpfKey DPF key type /// @tparam ReturnType return type /// @param return_type `return_entire_node_tag_{}` or `return_output_only_tag_{}`. /// @param dpf the DPF key /// @param begin the iterator to the first query /// @param end the iterator past the last query /// @return Pair of buffer (or tuple of buffers) and an iterable in list order. /// \complexity O(n k) interior traversals in the worst case and O(k) node workspace. k is the number of listed points and n is `depth`. The breadth-first buffer is 2k nodes, so each level traverses at most one node per point. Shared prefixes do fewer traversals. A recipe memoizer instead stores O(recipe leaf nodes) (see that memoizer). 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)); } /// \complexity O(n k) interior traversals in the worst case and O(k) node workspace. k is the number of listed points and n is `depth`. The breadth-first buffer is 2k nodes, so each level traverses at most one node per point. Shared prefixes do fewer traversals. A recipe memoizer instead stores O(recipe leaf nodes) (see that memoizer). 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; HEDLEY_PRAGMA(GCC diagnostic pop) using unique_ptr = typename allocator::unique_ptr; 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::vector splits; splits.reserve(nodes_in_sequence + 1); splits.push_back(begin); splits.push_back(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) }; const bool is_last = dpf_type::tree::is_last_level(level_index - 1, dpf_type::depth); // `lower` and `upper` are always adjacent elements of `splits` with `lower` < `upper` // [lower, upper) = "block" for (std::size_t s = 0; s + 1 < splits.size(); ++s) { auto lower = splits[s]; auto upper = splits[s + 1]; // `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, is_last); } 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, is_last); } 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], is_last); memo[curhalf*nodes_in_sequence + i++] = kids[0]; memo[curhalf*nodes_in_sequence + i++] = kids[1]; splits.insert(splits.begin() + static_cast(s + 1), it); ++s; // skip the newly inserted right-block start } } }; 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(); HEDLEY_PRAGMA(GCC diagnostic pop) 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 { utils::raw_memcpy(&outbuf[i*dpf_type::outputs_per_leaf], &leaf, sizeof(output_type)*dpf_type::outputs_per_leaf); } prev = curr++; } return subsequence_iterable(std::begin(outbuf), begin, end); } /// \complexity O(n k) interior traversals in the worst case and O(k) node workspace. k is the number of listed points and n is `depth`. The breadth-first buffer is 2k nodes, so each level traverses at most one node per point. Shared prefixes do fewer traversals. A recipe memoizer instead stores O(recipe leaf nodes) (see that memoizer). 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) }; const bool is_last = dpf_type::tree::is_last_level(level_index - 1, dpf_type::depth); 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, is_last); } 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, is_last); } } } } 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]; HEDLEY_PRAGMA(GCC diagnostic pop) 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 { utils::raw_memcpy(&outbuf[j*dpf_type::outputs_per_leaf], &leaf, sizeof(output_type)*dpf_type::outputs_per_leaf); } } } 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(); HEDLEY_PRAGMA(GCC diagnostic pop) 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); assign_share_slot(outbuf[i], v); } } 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) { (void)return_type; 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 /// @brief Evaluate `recipe` into a named buffer, reusing `memoizer`. /// @tparam I output index /// @tparam Is is /// @tparam DpfKey DPF key type /// @tparam OutputBuffers tuple of output buffers /// @tparam SequenceMemoizer sequence memoizer /// @tparam ReturnType return type /// @tparam SequenceMemoizer sequence memoizer /// @tparam ReturnType return type /// @param recipe The same object `memoizer` was constructed from. /// @param outbufs Named buffer. The returned iterable refers into it. /// @param dpf the DPF key /// @param memoizer the memoizer built for this key /// @param return_type `return_entire_node_tag_{}` or `return_output_only_tag_{}` /// @return the evaluation result /// \complexity O(n k) interior traversals in the worst case and O(k) node workspace. k is the number of listed points and n is `depth`. The breadth-first buffer is 2k nodes, so each level traverses at most one node per point. Shared prefixes do fewer traversals. A recipe memoizer instead stores O(recipe leaf nodes) (see that memoizer). 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)>()); } /// \complexity O(n k) interior traversals in the worst case and O(k) node workspace. k is the number of listed points and n is `depth`. The breadth-first buffer is 2k nodes, so each level traverses at most one node per point. Shared prefixes do fewer traversals. A recipe memoizer instead stores O(recipe leaf nodes) (see that memoizer). 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); } /// \complexity O(n k) interior traversals in the worst case and O(k) node workspace. k is the number of listed points and n is `depth`. The breadth-first buffer is 2k nodes, so each level traverses at most one node per point. Shared prefixes do fewer traversals. A recipe memoizer instead stores O(recipe leaf nodes) (see that memoizer). 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)); } /// \complexity O(n k) interior traversals in the worst case and O(k) node workspace. k is the number of listed points and n is `depth`. The breadth-first buffer is 2k nodes, so each level traverses at most one node per point. Shared prefixes do fewer traversals. A recipe memoizer instead stores O(recipe leaf nodes) (see that memoizer). 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); } /// @brief Fold a sorted sequence into `pr` via interval-run covers. /// @details Maximal contiguous runs use once-per-BFS-node `prove_fold_interval`. /// Isolated points are length-1 runs. Both parties must see the same /// sorted list. Prefer this over path-memo when the list is dense. template void prove_sequence(const KeyT & key, ForwardIterator begin, ForwardIterator end, prove_ref pr) { static_assert(KeyT::is_verifiable, "prove_sequence: key must carry dpf::verifiable"); if (HEDLEY_UNLIKELY(begin != end && !std::is_sorted(begin, end))) throw std::runtime_error("list must be sorted"); detail::vdpf::init_proof(pr.token, key); using input_type = typename KeyT::input_type; for (auto it = begin; it != end; ) { const auto run_from = static_cast(*it); auto run_to = run_from; ++it; while (it != end) { const auto next = static_cast(*it); if (next != static_cast(run_to + input_type{1})) break; run_to = next; ++it; } prove_fold_interval(key, run_from, run_to, pr.token); } detail::vdpf::fold_output_binding(pr.token, key); } } // namespace dpf #endif // LIBDPF_INCLUDE_DPF_EVAL_SEQUENCE_HPP__