libdpf/doc/pages/evaluation.md

198 lines
7.7 KiB
Markdown

<!-- # Evaluating DPFs {#evaluation} -->
`make_dpf(x, y)` returns one key per party. Evaluation of a key yields that
party's share. Leaf outputs are subtractive shares: open them with
`dpf::reconstruct`, which computes `share0 - share1`. Comparison outputs are
additive shares: `reconstruct` computes `share0 + share1`. A single-output
`eval_point` returns a small handle; `*handle` is the share. An unassigned
`dpf::wildcard` output throws `std::runtime_error`.
`[from, to]` is inclusive. The points passed to `eval_sequence` are a
nondecreasing range; an unsorted range throws `std::runtime_error`.
Memoizers hold interior nodes between calls. Output buffers hold the shares
a multi-point evaluation writes. Pass both as mutable named objects when a
later call should reuse them. The factories
`make_basic_path_memoizer`, `make_basic_interval_memoizer`, and
`make_*_sequence_memoizer` unwrap `party_key`, so a workspace built from
either party's type accepts both parties. Name that type with
`dpf::unwrap_party_key_t<std::decay_t<decltype(key)>>`.
# Memoizers {#memoizers}
## Path memoizers {#path_memoizers}
`eval_point` walks one root-to-leaf path. `make_basic_path_memoizer<Key>()`
keeps every node of the previous point, and the next point recomputes only
the suffix after the common prefix. `make_nonmemoizing_path_memoizer<Key>()`
keeps one node and starts from the root on every call. A one-off
`eval_point(key, x)` uses the nonmemoizing memoizer.
Pass the memoizer as a mutable lvalue. The default argument is a new
temporary, so it has no previous point to resume from. Keep a separate
memoizer for each key you are in the middle of evaluating. A different root
restarts the path.
**Code samples**\n
<div class="tabbed">
- <b class="tab-title">memoizers.cpp</b> \include{cpp} evaluation/memoizers.cpp
</div>
## Interval memoizers {#interval_memoizers}
`eval_interval` and `eval_full` expand every leaf in a range.
`make_basic_interval_memoizer<Key>(from, to)` stores two levels of that
range. That is the workspace the convenience overloads allocate.
`make_full_tree_interval_memoizer<Key>(from, to)` keeps every level.
`make_basic_full_memoizer<Key>()` and `make_full_tree_full_memoizer<Key>()`
are the same workspaces sized for the whole domain.
Size the memoizer for the widest interval you will pass to it. A wider
interval throws `std::length_error`. The same key and the same endpoints
leave the final interior level in place. A different key or a different
interval rebuilds into the same allocation.
Passing only the memoizer still allocates a fresh output buffer and returns
`std::pair(buffer, iterable)`.
## Sequence memoizers {#sequence_memoizers}
`make_sequence_recipe<Key>(begin, end)` compiles a sorted point list into a
traversal. The recipe depends on the input type, and one recipe serves every
key of that type.
A sequence memoizer stores a reference to the recipe object it was built
from and checks later calls by address. Pass that same object, and keep the
recipe alive for as long as the memoizer is used. A copy of the recipe
throws `std::logic_error`.
`make_double_space_sequence_memoizer<Key>(recipe)` keeps two levels. It is
what `eval_sequence(key, recipe, buffer)` allocates when you omit the
memoizer. `make_inplace_reversing_sequence_memoizer<Key>(recipe)` keeps one
level and reverses direction as it descends.
`make_full_tree_sequence_memoizer<Key>(recipe)` retains every level. A key
whose depth differs from the recipe throws `std::logic_error`.
# Output buffers {#output_buffers}
`output_buffer<T>` is move-only storage with `size`, iterators, `data`, and
`operator[]`. Build it with the factory that matches the evaluation:
- `make_output_buffer_for_interval(key, from, to)`
- `make_output_buffer_for_full(key)`
- `make_output_buffer_for_subsequence(key, begin, end, tag)`
- `make_output_buffer_for_recipe_subsequence(key, recipe, tag)`
On a `party_key`, leaf slots are `subtractive_share`s and comparison slots
are `additive_share`s. `dpf::bit`, `dpf::twobit`, and `dpf::nyble` slots are
packed. Trivially default-constructible slot types are left uninitialized;
the evaluation overwrites every slot it is responsible for.
`eval_interval` and recipe `eval_sequence` take the buffer as a non-const
reference, so the argument is a named object. The returned iterable refers
into that buffer. Read it while the buffer is alive, and only over the
points the iterable covers. The next evaluation overwrites those slots.
For one output, the convenience overload returns the buffer itself as the
first element of the pair. For several output indices it returns a tuple of
buffers.
`make_output_buffer(dpf::out<I>, key, from, to)` and
`make_output_buffer(dpf::cmp, key, n)` size a buffer for one channel of a
multi-output or comparison key. The slot types follow the same party-share
rule.
**Code samples**\n
<div class="tabbed">
- <b class="tab-title">output_buffers.cpp</b> \include{cpp} evaluation/output_buffers.cpp
</div>
# dpf::eval_point {#eval_point}
`eval_point(key, x)` evaluates output 0 at one input.
`eval_point<I>(key, x)` selects another output. Two or more indices,
`eval_point<0, 1>(key, x)`, return a tuple of shares rather than handles.
`eval_point(key, x, path)` continues a path memoizer.
`eval_point(dpf::out<I>, key, x, path)` and `eval_point(dpf::cmp, key, x, path)`
are the same walk with an explicit channel. Comparison results are additive
shares.
**Code samples**\n
<div class="tabbed">
- <b class="tab-title">eval_point.cpp</b> \include{cpp} evaluation/eval_point.cpp
</div>
# dpf::eval_interval {#eval_interval}
`eval_interval(key, from, to)` evaluates every input from `from` through
`to`. The iterable yields one share per input, in that order. Optional
arguments are an output buffer and then an interval memoizer. An output
index pack, `eval_interval<0, 1>(key, from, to, buffers, memo)`, writes each
selected output.
**Code samples**\n
<div class="tabbed">
- <b class="tab-title">eval_interval.cpp</b> \include{cpp} evaluation/eval_interval.cpp
</div>
# dpf::eval_full {#eval_full}
`eval_full(key)` is the closed interval from
`std::numeric_limits<Input>::min()` through `max()`. The buffer and
full-domain memoizer overloads match `eval_interval`.
`make_output_buffer_for_full(key)` and `make_basic_full_memoizer<Key>()`
size both for that domain.
**Code samples**\n
<div class="tabbed">
- <b class="tab-title">eval_full.cpp</b> \include{cpp} evaluation/eval_full.cpp
</div>
# dpf::eval_sequence {#eval_sequence}
`eval_sequence(key, begin, end, tag)` evaluates a sorted list.
`dpf::return_output_only_tag_` stores one share per listed point.
`dpf::return_entire_node_tag_` stores whole leaves; it is the default.
The iterable still yields one share per listed point, in list order.
`eval_sequence(key, recipe, buffer, memo, tag)` repeats that list.
`memo` is a sequence memoizer bound to `recipe`. Omit `memo` to allocate a
`double_space` workspace for that call.
**Code samples**\n
<div class="tabbed">
- <b class="tab-title">eval_sequence.cpp</b> \include{cpp} evaluation/eval_sequence.cpp
</div>
# Buffered PRG {#buffered_prg}
`dpf::randomness::buffered_prg<PRG, Ts...>` (alias
`dpf::randomness::aes_buffered_prg<Ts...>`) is a forward cursor with one
PRG stream per value type. `get<I>()` and `fill<I>(out, n)` consume the
cursor. `at<I>(index)` reads an absolute index and leaves the cursor where
it is. `sampled<I>()` is how far `get` and `fill` have advanced.
`per_stream_buffer_elems` is at least 1.
`dpf::randomness::lane_table<T>` is the seekable form for a runtime set of
roles. `value_at(role, index)` and `mask_at(role, index)` are independent
streams, and a repeated index returns the same element.
**Code samples**\n
<div class="tabbed">
- <b class="tab-title">buffered_prg.cpp</b> \include{cpp} evaluation/buffered_prg.cpp
</div>