SEAL全同态加密开源库(三) NTT代码分析
2021SC@SDUSC
2021-10-17
前言
这是SEAL开源库代码分析报告第三篇,本篇将首先介绍一下编译运行的最终版的成果,借此对第一篇博客的部分编译内容加以修正。然后本篇将重点介绍SEAL库多项式乘法的重要基础算法:NTT。注意这里将涉及大量的理论基础,可能略显枯燥困难。
编译步骤的修正
在第一篇博客中,讲述了编译运行SEAL库的完整步骤,但是并未经过实战检验,经过检验,发现部分内容存在漏洞,在经过很久的尝试之后,终于对最新版的SEAL库(version:3.7)实现了编译运行,这在网络上还是很难找到相关的资料的,因此我觉得有必要加以记录下来。
当时的步骤是这样的:
但是后来发现这样是不对的,原因在于release版本和debug版本的编译结果并不能完全混用,因此我重新想办法解决了这个问题:
完美运行SEAL库示例
打开cmake编译后的SEAL工程,找到INSTALL,这个时候要注意,我们要选择release编译(VS没有,那就在config里面新建一个release的编译选项),右键INSTALL 点击生成。然后等待显示成功编译后,我们回到这个目录 就会发现生成了release所对应的lib库,(再说一次,带d的是debug的,不带d的是release),接下来就可以进行release的配置了。完美!
其中可能会遇到报错,解决方案:用管理员身份打开VS2019即可。
目前终于完全解决了安装配置的问题。
可以做到新建项目文件中导入SEAL作为第三方库。(虽然短期看来没什么用,主要是为了验证安装配置的正确性)
example文件夹可以跑了。
把该文件夹拷贝出来,用VS只打开该文件夹,会自动执行cmake,当然,注意配置cmake时,一定要选择Release-x64(默认的是debug-x64),因为我们引用的库是release版本的。
打开sealexample.cpp,点击运行,编译无误,即可生成.exe文件运行。
注意,网上的例子绝大多数,导包是没问题的,都不能成功运行,因为版本问题。最好还是跑3.7包里面的example。
FTT
由于NTT是对FTT的优化,因此在讲解NTT之前,有必要先讲解FTT——快速傅里叶变换。
快速傅里叶变换 (fast Fourier transform),即利用计算机计算离散傅里叶变换(DFT)的高效、快速计算方法的统称,简称FFT。快速傅里叶变换是1965年由J.W.库利和T.W.图基提出的。采用这种算法能使计算机计算离散傅里叶变换所需要的乘法次数大为减少,特别是被变换的抽样点数N越多,FFT算法计算量的节省就越显著。
FFT(Fast Fourier Transformation) 是离散傅氏变换(DFT)的快速算法。即为快速傅氏变换。它是根据离散傅氏变换的奇、偶、虚、实等特性,对离散傅立叶变换的算法进行改进获得的。
——百度百科
FFT(Fast Fourier Transformation),中文名快速傅里叶变换,用来加速多项式乘法。
在这里在读完相关文献入门之后,我先给出我个人的一点理解:
任意多项式由系数形式转换为点值形式,常规代入n个不同数的复杂度是O(n^2),原因是n个数,每个数n项,相乘就是平方。
通过Omega的特殊性质,我们可以将带入的数选定为Omega的k(k:1-n)次方,这样还是n个数,但每个数的复杂度降为logn(分治了,每次分治砍一半,回溯看一半的结果即可推得下一半,所以2^x=n,x是对数形式),所以总复杂度化为了nlongn。
受限于篇幅,仅介绍一些重要的理论基础:
对于两个用系数表示的多项式,我们把它们相乘,设两个多项式分别为A ( x ) , B ( x ) ,我们要枚举A每一位的系数与B每一位的系数相乘,那么系数表示法做多项式乘法时间复杂度O(n^2),但两个用点值表示的多项式相乘,只需要O(n)的时间。
这里设计一些复数、极坐标的理论基础,不再赘述。
重点是掌握这几个公式:
OK,掌握了这些基本性质之后,我们把A(X)里面的x替换成单位根的k次方,看看有什么可以化简的地方,这也就是FTT的牛逼之处:
IFFT
我们把两个多项式相乘 (也叫求卷积),做完两遍F F T FFTFFT也知道了积的多项式的点值表示。下面要解决的问题是,怎么把点值表示的多项式快速转回系数表示法?
先给结论:
一个多项式在分治的过程中乘上单位根的共轭复数,分治完的每一项除以n nn即为原多项式的每一项系数。
所以FFT和IFFT可以一起完成。
下面给出代码实现,基本上就是分治递归的思想,当然进一步优化加入了迭代,非常巧妙:
void fft(cp *a,int n,int inv)
{
int bit=0;
while ((1<<bit)<n)bit++;
fo(i,0,n-1)
{
rev[i]=(rev[i>>1]>>1)|((i&1)<<(bit-1));
if (i<rev[i])swap(a[i],a[rev[i]]);//不加这条if会交换两次(就是没交换)
}
for (int mid=1;mid<n;mid*=2)//mid是准备合并序列的长度的二分之一
{
cp temp(cos(pi/mid),inv*sin(pi/mid));//单位根,pi的系数2已经约掉了
for (int i=0;i<n;i+=mid*2)//mid*2是准备合并序列的长度,i是合并到了哪一位
{
cp omega(1,0);
for (int j=0;j<mid;j++,omega*=temp)//只扫左半部分,得到右半部分的答案
{
cp x=a[i+j],y=omega*a[i+j+mid];
a[i+j]=x+y,a[i+j+mid]=x-y;//这个就是蝴蝶变换什么的
}
}
}
}
NTT
简介:
一种快速数论变换算法,这种算法是以数论为基础,对样本点为的数论变换,按时间抽取的方法,得到一组等价的迭代方程,有效高速简化了方程中的计算公式·与直接计算相比,大大减少了运算次数。(见快速傅里叶变换)。
在计算机实现多项式乘法中,我们所熟知的快速傅里叶变换(FFT)是基于n次单位根 (omega) 的优秀性质实现的,而由于其计算时会使用正弦函数和余弦函数,在不断运算时无法避免地会产生精度误差。而多项式乘法有些时候会建立在模域中,在对一些特殊的大质数取模时,便可以考虑用原根g来代替 ,而这些特殊的大质数的原根恰好满足 的某些性质,这使得多项式乘法在模域中也可以快速的分治合并。
——百度百科
和FFT一样,NTT也用来加速多项式乘法,不过NTT最大的优点是可以取模,或者可以理解为NTT是FFT的取模升级版。
其优点如下:
- 能取模,FFT的复数无法取个模
- 没有精度差,F F T FFTFFT浮点数的精度怎么也会出一点问题
- 由于均为整数操作(虽然取模多)NTT常数小,通常比一大堆浮点运算的FFT要快
因此,我们的SEAL库选用NTT来完成多项式乘法。
理论基础,涉及到数论的一些知识,重点是原根。
原根是一种数学符号,设m是正整数,a是整数,若a模m的阶等于φ(m),则称a为模m的一个原根。(其中φ(m)表示m的欧拉函数)
假设一个数g是P的原根,那么g^i mod P的结果两两不同,且有 1<g<P,0<i<P,归根到底就是g^(P-1) = 1 (mod P)当且仅当指数为P-1的时候成立.(这里P是素数)。
简单来说,g^i mod p ≠ g^j mod p (p为素数),其中i≠j且i, j介于1至(p-1)之间,则g为p的原根。
所以可以把g^{(p-1)/n}看成w n 1 的等价。其他的思想、步骤与FTT大部分相同。
贴代码就知道了:
inline void ntt(int a[],int len,int inv)
{
int bit=0;
while ((1<<bit)<len)++bit;
fo(i,0,len-1)
{
rev[i]=(rev[i>>1]>>1)|((i&1)<<(bit-1));
if (i<rev[i])swap(a[i],a[rev[i]]);
}//前面和FFT一样
for (int mid=1;mid<len;mid*=2)
{
int tmp=pow(g,(mod-1)/(mid*2));//原根代替单位根
if (inv==-1)tmp=pow(tmp,mod-2);//逆变换则乘上逆元
for (int i=0;i<len;i+=mid*2)
{
int omega=1;
for (ll j=0;j<mid;++j,omega=omega*tmp%mod)
{
int x=a[i+j],y=omega*a[i+j+mid]%mod;
a[i+j]=(x+y)%mod,a[i+j+mid]=(x-y+mod)%mod;//注意取模
}
}//大体和FFT差不多
}
}
后记
以上是有关NTT一些核心的理论、代码,至于SEAL中对NTT的实现则复杂得多,详见下一篇博客。