专栏文章
我的乘法取模方式果然有问题
科技·工程参与者 7已保存评论 8
文章操作
快速查看文章及其快照的属性,并进行相关操作。
- 当前评论
- 8 条
- 当前快照
- 1 份
- 快照标识符
- @mip2f4l2
- 此快照首次捕获于
- 2025/12/03 05:03 3 个月前
- 此快照最后确认于
- 2025/12/03 05:03 3 个月前
众所周知,模数为
CPPlong long 的乘法取模可以使用 __int128 ,也可以使用一些手法操作一下(即浮点数运算得到下取整的结果并使用自然溢出),后者的具体实现如下面这份代码所示。inline long long mul(long long a,long long b)
{
long long ret=a*b-((long long)(double(a)/Mod*b+0.5))*Mod;
return ret+(ret<0)*Mod;
}
特别的,我们默认模数不是特别大(如 左右),否则需要使用
long double 提高精度。考虑下面这份代码,它比较了上述两种乘法取模的实现方式的运行效率。
CPP#include<iostream>
#include<random>
using namespace std;
mt19937_64 Rand(123);
const int N=3e4,V=50;
const long long mod=(1ll<<V)+123456789;
long long Mod=mod,a[N+10],b[N+10];
inline long long mul(long long a,long long b)
{
long long ret=a*b-((long long)(double(a)/Mod*b+0.5))*Mod;
return ret+(ret<0)*Mod;
}
inline long long Mul(long long a,long long b)
{
return __int128(a)*b%mod;
}
int main()
{
for(int i=1;i<=N;i++)
{
a[i]=Rand()&(1ll<<V)-1;
b[i]=Rand()&(1ll<<V)-1;
}
long long ans=0;
clock_t Start=clock();
for(int i=1;i<=N;i++)
for(int j=1;j<=N;j++)
ans^=mul(a[i],b[j]);
cout<<clock()-Start<<endl;
Start=clock();
for(int i=1;i<=N;i++)
for(int j=1;j<=N;j++)
ans^=Mul(a[i],b[j]);
cout<<clock()-Start<<endl;
cout<<ans<<endl;
return 0;
}
在
AtCoder 的 Custom Test 上测试,语言选的是 C++ 23 (Clang 16.0.6) 。运行后输出如下结果。
CPP362830
3579920
0
(注:该评测机的
CLOCKS_PER_SEC 是 ,即总运行时间不到 秒。)不难发现,
__int128 的运行时间大约是 double 的十倍甚至九倍,我对此还是比较惊讶的。顺带一提,就算是使用
long double 也是比 __int128 快的。使用上述评测环境,输出结果如下,大概是
CPP__int128 运行时间的三分之二。2534783
3675875
0
我们回到第一份代码的输出结果,仔细观察后发现,
double 竟然只跑了 秒左右。这是什么概念?这又意味着什么呢?
我们在没有使用循环展开等奇技淫巧且只开启了
O2 优化的情况下,计算 次模数为 long long 的乘法取模只使用了 秒!换句话说,在一秒的时间内,我们的程序可以进行约 次模数为 long long 乘法取模!你可能对乘法取模的速度没什么概念,那么我们来看看我们熟知的模数在
CPPint 级别的乘法取模。#include<iostream>
#include<random>
using namespace std;
mt19937 Rand(123);
#define rand Rand
const int N=3e4,V=29,mod=1e9+7;
int Mod=mod,a[N+10],b[N+10];
inline int mul(int a,int b)
{
int ret=a*b-(int(double(a)/Mod*b+0.5))*Mod;
return ret+(ret<0)*Mod;
}
inline int Mul(int a,int b)
{
return (long long)a*b%mod;
}
int main()
{
for(int i=1;i<=N;i++)
{
a[i]=Rand()&(1<<V)-1;
b[i]=Rand()&(1<<V)-1;
}
int ans=0;
clock_t Start=clock();
for(int i=1;i<=N;i++)
for(int j=1;j<=N;j++)
ans^=mul(a[i],b[j]);
cout<<clock()-Start<<endl;
Start=clock();
for(int i=1;i<=N;i++)
for(int j=1;j<=N;j++)
ans^=Mul(a[i],b[j]);
cout<<clock()-Start<<endl;
cout<<ans<<endl;
return 0;
}
使用同样的评测环境,运行后的输出结果如下。
CPP257907
963987
0
???
我不懂,但我大受震撼。
下面是笔者自己的一点猜测。
我怀疑编译器(
Clang )进行了一些操作使得大量对 mul() 函数的调用得以并行计算。可能与循环展开本质类似。
(注:疑似该程序使用
gcc 编译器时的运行效率会大大降低。)(更新于 2025/06/21 。)
昨天凌晨破防的时候我发现,由于
CPPa[i] 的取值与 无关,故编译器可能会优化掉一些重复的运算(如 double(a)/Mod 等),对此我们可以考虑改为调用 mul(a[i]^b[j],b[j]) 。若编译环境保持不变,上述三份代码运行后的输出结果依次如下。554456
3351854
0
CPP3485189
3347934
0
CPP526950
1211967
0
不难发现,更改后运行效率确实有显著降低,看上去也更符合我对乘法取模的想象了。
(虽然感觉还是有点快。)
特别的,
long double 实现的乘法取模不再能跑过 __int128 。(注:现在疑似该程序使用
gcc 编译器时的运行效率与使用 Clang 时无显著区别。)详细揭秘:关于我在无意间被浮点数的指令集变成废柴这件事。
wow 好厉害。
相关推荐
评论
共 8 条评论,欢迎与作者交流。
正在加载评论...