速度是编译器实现两倍的取模算法
CF blog
不是直接取模,是解决模乘的问题:
给定
k,m 和n 个数a_i ,算每个a_i\times k\bmod m
笔者想到了一个计算方法,比编译器的实现(当
首先
其中
设
这里有两个选择题。
-
- 最后一个式子算出来不一定是整数,要下取整还是上取整。
我们选择
定理:令
p=\lceil\frac km\times2^{64}\rceil , 当a_i\le \frac{2^{64}}m 时,下取整的计算结果是准确的。证明:写外面了
代码实现:
const int P = 998244353;
void calc(int n, int k, int a[]) {
unsigned long long p = (((unsigned __int128)k << 64) + P - 1) / P;
for(int i = 0; i < n; i++)
a[i] = (unsigned)a[i] * p * (unsigned __int128)P >> 64;
}
几点说明:
- 代码中用了
unsigned __int128所以必须要 64 位架构才能用。 (unsigned)a[i] * p是unsigned long long自然溢出,底层是 64 位乘法只保留低 64 位。* (unsigned __int128)P >> 64底层是 64 位乘法只保留高 64 位(rdx 寄存器),和自然溢出速度一样。- 把
a[i]转成unsigned是因为int先转unsigned再转unsigned long long比直接转unsigned long long更快,后者需要 符号扩展。
速度测试。
测试方式和代码,包含两个部分:
- 第一个部分是吞吐量 (Throughput),CPU 高度并行的用时(现代 CPU 单核也能指令级并行),共包含
50000\times50000 次模乘。 - 第二个部分是延迟 (Latency),每次模乘依次进行而不并行的用时,共包含
50000\times25000 次模乘。
可能的输出:
Throughput test(50000 * 50000):
Compiler's signed modulo: 1954.83 ms
Compiler's unsigned modulo: 1746.73 ms
My modulo: 1160.47 ms
Latency test(50000 * 25000):
Compiler's signed modulo: 4329.33 ms
Compiler's unsigned modulo: 3945.29 ms
My modulo: 2397.97 ms
顺便普及一下几个常识:
int先转unsigned再转long long比直接转long long更快,但负数会出错。unsigned long long对 常量 取模比long long快。- 常量模乘的并行速度几乎是串行的
5 倍(笔者发明的模乘也是)。
扩展
还可以计算
令