安全之安全(security²)博客目录导读
目录
@par PolyIter, RNSIter 和 CoeffIter
@par PtrIter 和 StrideIter
@par IterTuple
@par 常见的 PtrIter 类型的别名
@par 创建 SEAL 迭代器
@par 使用 ReverseIter 反转方向
@par SEAL_ITERATE
@par 编码约定
@par 常见函数的迭代器重载
@par 使用 SeqIter 进行索引
@par 关于分配的注意事项
@par 指向临时分配的迭代器
@par PolyIter, RNSIter 和 CoeffIter
在这个文件中,我们定义了一组自定义迭代器类(“SEAL 迭代器”),用于在 Microsoft SEAL 中更方便地遍历密文多项式、它们的 RNS 组件以及 RNS 组件中的系数。所有 SEAL 迭代器都满足 C++ LegacyRandomAccessIterator 的要求。SEAL 迭代器非常适合与 SEAL_ITERATE 宏一起使用,该宏在 C++17 中扩展为 std::for_each_n,在 C++14 中扩展为 seal::util::seal_for_each_n。所有 SEAL 迭代器都派生自 SEALIterBase。
最重要的 SEAL 迭代器类的行为如下图所示:
+-------------------+
| 指针和大小 | 构造 +-----------------+
| 或密文 |------>| (Const)PolyIter | 遍历密文中的 RNS 多项式
+-------------------+ +--------+--------+ (coeff_modulus_size 个 RNS 组件)||| 解引用||v+----------------+ 构造 +----------------+| 指针和大小 |------>| (Const)RNSIter | 遍历 RNS 多项式中的 RNS 组件+----------------+ +-------+--------+ (poly_modulus_degree 个系数)||| 解引用||v+----------------+ 构造 +------------------+| 指针和大小 |------>| (Const)CoeffIter | 遍历单个 RNS 多项式组件中的系数 (std::uint64_t)+----------------+ +---------+--------+||| 解引用||v+-------------------------+| (const) std::uint64_t & |+-------------------------+
@par PtrIter 和 StrideIter
PtrIter<T *> 和 StrideIter<T *> 都是模板化的 SEAL 迭代器,它们包装了原始指针。这两者的区别在于,PtrIter<T *> 的步长总是1,而 StrideIter<T *> 的步长可以设置为任意值。CoeffIter 是 PtrIter<std::uint64_t *> 的 typedef,而 RNSIter 与 StrideIter<std::uint64_t *> 几乎相同,但仍然是不同的类型。
+----------+ 构造 +-------------------+
| MyType * |------>| PtrIter<MyType *> | 原始指针的简单包装
+----------+ +----+----------+---+| || |解引用 | | PtrIter<MyType *>::ptr()| | 或隐式转换| |v v+----------+ +----------+| MyType & | | MyType * |+----------+ +----------++----------+ 构造 +----------------------+
| MyType * |------>| StrideIter<MyType *> | 带自定义步长的原始指针的简单包装
+----------+ +-----+----------+-----+| || |解引用 | | StrideIter<MyType *>::ptr()| | 或隐式转换| |v v+----------+ +----------+| MyType & | | MyType * |+----------+ +----------+
@par IterTuple
一个非常有用的模板类是(可变参数的)IterTuple<...>
,它允许多个 SEAL 迭代器一起使用。IterTuple
本身就是一个 SEAL 迭代器,并且在库中常常使用嵌套的 IterTuple
类型。解引用 IterTuple
总是返回一个 std::tuple
,其中每个 IterTuple
元素都被解引用。由于 IterTuple
可以从一个包含每个迭代器的单参数构造函数参数的 std::tuple
构造,所以解引用后的 std::tuple
通常可以直接传递给期望 IterTuple
的函数。
IterTuple
的各个组件可以通过 seal::util::get<i>(...)
函数访问。IterTuple
的行为概述如下图所示:
+-----------------------------------------+| IterTuple<PolyIter, RNSIter, CoeffIter> |+--------------------+--------------------+||| 解引用||v+--------------------------------------------------+| std::tuple<RNSIter, CoeffIter, std::uint64_t &>> |+------+-------------------+-------------------+---+| | || | || std::get<0> | std::get<1> | std::get<2>| | || | |v v v+-------------+ +---------------+ +-----------------+| RNSIter | | CoeffIter | | std::uint64_t & |+-------------+ +---------------+ +-----------------+
有时候我们需要使用多个嵌套的迭代器元组。在这种情况下,使用嵌套的 get<...>
调用来访问嵌套的迭代器会很繁琐。考虑以下情况,其中 encrypted1
和 encrypted2
是 Ciphertexts
,destination
是一个 Ciphertext
或 PolyIter
:
IterTuple<PolyIter, PolyIter> I(encrypted1, encrypted2);
IterTuple<decltype(I), PolyIter> J(I, destination);
auto encrypted1_iter = get<0>(get<0>(J));
auto encrypted2_iter = get<1>(get<0>(J));
一种更简单的方法是使用另一种形式的 get<...>
,它接受多个索引并以嵌套的方式访问结构。例如,在上述情况下,我们也可以这样写:
auto encrypted1_iter = get<0, 0>(J));
auto encrypted2_iter = get<0, 1>(J));
请注意,最内层元组的索引首先出现在列表中,即顺序与嵌套 get<...>
调用中出现的顺序相反。这种反转的原因是,当推断迭代器是什么时,首先检查最内层的范围,最后检查最外层的范围,这与索引的顺序相对应。我们还提供了类似的函数,用于访问嵌套的 std::tuple
对象,这在访问嵌套的 IterTuple
解引用时是必要的。
@par 常见的 PtrIter
类型的别名
很常见的类型是 PtrIter<Modulus *>
和 PtrIter<NTTTables *>
。为了简化表示,我们为这些类型设置了别名:ModulusIter
和 NTTTablesIter
。还有const版本 ConstModulusIter
和 ConstNTTTablesIter
,分别包装指向常量 Modulus
和 NTTTables
的指针。
@par 创建 SEAL 迭代器
使用可变参数的 iter
函数创建迭代器是最简单的,当给定一个或多个可以自然转换为 SEAL 迭代器的参数时,它会输出一个适当的迭代器或迭代器元组。再次考虑上面的代码片段,以及编写模板参数可能变得多么混乱。相反,我们可以简单地写:
auto I = iter(encrypted1, encrypted2);
auto J = iter(I, destination);
auto encrypted1_iter = get<0, 0>(J));
auto encrypted2_iter = get<0, 1>(J));
从 iter
函数创建 IterTuple
有三种方法。第一种方法是传递一个 IterTuple
作为输入,在这种情况下 iter
输出它的副本;没有理由这样做。第二种方法是传递一组可变参数构造函数参数;iter
将输出一个由与给定构造函数参数兼容的 SEAL 迭代器组成的 IterTuple
。第三种方法是传递一个包含一组可变参数构造函数参数的 std::tuple
;行为与第二种方法相同。
@par 使用 ReverseIter
反转方向
除了上面描述的迭代器类型外,我们还提供了 ReverseIter<SEALIter>
,它可以反转迭代方向。ReverseIter<SEALIter>
解引用为与 SEALIter
相同的类型:例如,解引用 ReverseIter<RNSIter>
结果是 CoeffIter
,而不是 ReverseIter<CoeffIter>
。
使用 reverse_iter
函数可以很容易地从给定的 SEAL 迭代器创建一个 ReverseIter
。例如,reverse_iter(encrypted)
将返回一个 ReverseIter<PolyIter>
,如果 encrypted
是一个 PolyIter
或 Ciphertext
。当传递多个参数时,reverse_iter
返回一个适当的 ReverseIter<IterTuple<...>>
。例如,如果 encrypted1
和 encrypted2
是 PolyIter
或 Ciphertext
对象,那么 reverse_iter(encrypted1, encrypted2)
返回 ReverseIter<IterTuple<PolyIter, PolyIter>>
。
@par SEAL_ITERATE
SEAL 迭代器是为了与 SEAL_ITERATE
宏一起使用而设计的,以迭代一定数量的步骤,并在每一步调用给定的 lambda 函数。在 C++17 中,SEAL_ITERATE
展开为 std::for_each_n
,在 C++14 中,它展开为 seal::util::seal_for_each_n
-- 一个自定义实现。例如,以下代码片段出现在 Evaluator::bfv_multiply
中:
SEAL_ITERATE(iter(encrypted1, encrypted1_q, encrypted1_Bsk),encrypted1_size,behz_extend_base_convert_to_ntt);
这里,使用 iter
函数创建了一个 IterTuple<PolyIter, PolyIter, PolyIter>
;参数类型是 Ciphertext
(encrypted1
)、PolyIter
(encrypted1_q
)和 PolyIter
(encrypted1_Bsk
)。迭代器前进 encrypted1_size
次,每次都调用 lambda 函数 behz_extend_base_convert_to_ntt
,并解引用迭代器元组。lambda 函数如下开始:
auto behz_extend_base_convert_to_ntt = [&](auto I) {set_poly(get<0>(I), coeff_count, base_q_size, get<1>(I));ntt_negacyclic_harvey_lazy(get<1>(I), base_q_size, base_q_ntt_tables);...
});
这里,参数 I
的类型是 IterTuple<RNSIter, RNSIter, RNSIter>(解引用后的)
。在 lambda 函数中,我们首先将 RNS 多项式从 get<0>(I)
(encrypted1
)复制到 get<1>(I)
(encrypted1_q
),并将其转换为 NTT 形式。我们使用 ntt_negacyclic_harvey_lazy
的一个重载,它接受 RNSIter
、RNS 基的大小和 ConstNTTTablesIter
作为参数,并分别转换每个 RNS 组件。查看 seal/util/ntt.h
,我们看到函数 ntt_negacyclic_harvey_lazy
再次使用了 SEAL_ITERATE
实现。具体来说,它包含以下内容:
SEAL_ITERATE(iter(operand, tables), coeff_modulus_size, [&](auto I) {ntt_negacyclic_harvey_lazy(get<0>(I), get<1>(I));
});
在这里,iter
输出一个 IterTuple<RNSIter, ConstNTTTablesIter>
。在这种情况下,要调用的lambda函数是内联定义的。参数 I
取值 IterTuple<CoeffIter, const NTTTables *>
,并且每一步调用 ntt_negacyclic_harvey_lazy
的 CoeffIter
重载,带有对匹配的 NTTTables
对象的引用。
@par 编码约定
在上面的代码片段中,有两个重要的编码约定需要遵循:
-
使用
I
、J
、K
等作为表示 SEAL 迭代器的 lambda 函数参数。这简洁明了,并且使得在 SEAL 的其他上下文中不应使用这些变量名称,从而明确这些对象是 SEAL 迭代器。 -
传递给
SEAL_ITERATE
的 lambda 函数几乎总是(参见第 3 点)接受类型为auto
的参数。这将生成外观简单且性能良好的代码,达到预期的结果。 -
第 2 点的唯一例外是当
SEAL_ITERATE
操作单个PtrIter<T *>
时:解引用返回一个T &
,这可能需要通过引用传递给 lambda 函数。有关示例,请参见seal::util::ntt_negacyclic_harvey
中的seal/util/ntt.h
,其中 lambda 函数参数是auto &
。注意:
IterTuple<PolyIter, CoeffIter>
将解引用为std::tuple<RNSIter, std::uint64_t &>
,可以安全地按值传递给 lambda 函数。因此,lambda 函数中类型为auto
的参数将很可能按预期工作。注意:另一种始终正确的方法是使用转发引用
auto &&
作为 lambda 函数参数。然而,我们认为这会对代码进行不必要的复杂化,收益甚微。
@par 常见函数的迭代器重载
有些函数具有重载,直接接受CoeffIter、RNSIter或PolyIter输入,并将相关操作应用于迭代器所指示的整个结构。例如,函数seal::util::negate_poly_coeffmod可以对给定的模数取反单个RNS组件模(CoeffIter重载),对匹配的模数元素数组取反整个RNS多项式模(RNSIter重载),或对RNS多项式数组取反(PolyIter重载)。
@par 使用 SeqIter 进行索引
有时在 SEAL_ITERATE
lambda 函数内部,知道迭代的索引是很方便的。这可以使用 SeqIter<T>
迭代器来完成。模板参数是索引计数器的算术类型。
创建 SeqIter
对象的最简单方法是使用 seq_iter
函数。例如,seq_iter(0)
返回一个初始值为 0 的 SeqIter<int>
对象。或者,iter
函数将检测传递给它的算术类型,并从中创建 SeqIter
对象。例如,调用 iter(0)
等效于调用 seq_iter(0)
,这也适用于 iter
的多参数调用。解引用一个 SeqIter
对象返回当前值。对于反向索引,只需将 SeqIter
包装到 ReverseIter
中,或直接调用带有起始索引的 reverse_iter
。
@par 关于分配的注意事项
未来我们希望使用 C++17 引入的并行版本 std::for_each_n
。为了实现这一点,请注意在 lambda 函数中如何使用堆分配。特别是在繁重的 lambda 函数中,最好在 lambda 函数内部调用 seal::util::allocate
进行所需的任何分配,而不是使用从外部捕获的分配。
@par 指向临时分配的迭代器
在许多情况下,可能希望分配一个临时缓冲区并创建一个指向它的迭代器。但是,现在必须注意为分配和设置迭代器使用正确的大小参数。为此,我们提供了一些有用的宏,这些宏设置指针并仅向函数公开迭代器。例如,代替编写以下容易出错的代码:
auto temp_alloc(allocate_poly_array(count, poly_modulus_degree, coeff_modulus_size, pool));
PolyIter temp(temp_alloc.get(), poly_modulus_degree, coeff_modulus_size);
我们可以简单地写:
SEAL_ALLOCATE_GET_POLY_ITER(temp, count, poly_modulus_degree, coeff_modulus_size, pool);
然而,后者不公开分配本身的名称。还有类似的宏用于分配缓冲区并设置 PtrIter<T *>
、StrideIter<T *>
、RNSIter
和 CoeffIter
对象。