一些卡常技巧
什么?你说这些东西没用?
那你就大错特错了。WC考过的东西怎么可能没用
NTT时加法取模用
ll add(ll a,ll b){a+=b;return (a>=b?a-=p:0),a;}
会比
ll add(ll a,ll b){return (a+b)%p;}
快 \(20\%\)(我的写法),减法同理。
开O2之后FFT会比不开快几倍
不开O2:NTT比FFT快
开O2:FFT比NTT快
常数尽量声明成常量
有一道NTT的题,模数声明成变量跑了\(1166\)ms,模数声明成常量跑了不到\(300\)ms
//6s
const int p=10;
int main()
{
open("orzzjt");
int a;
scanf("%d",&a);
int i;
for(i=1;i<=1000000000;i++)
a=(a*a+10)%p;
printf("%d\n",a);
return 0;
}
//10s
int p=10;
int main()
{
open("orzzjt");
int a;
scanf("%d",&a);
int i;
for(i=1;i<=1000000000;i++)
a=(a*a+10)%p;
printf("%d\n",a);
return 0;
}
能用位运算尽量用位运算
当然,编译器大多数情况下会帮你优化掉。
少用除法和取模
加法运算只要\(1\)个时钟周期,乘法运算只要\(3\)个时钟周期,而除法和取模运算要几到几十个时钟周期。
\(3\times 3\)的矩阵乘法:边加边取模:\(27\)次取模运算;全部算完再取模:\(9\)次取模运算。
优化高位数组的寻址
用指针保存上一次使用的地址,直接加偏移。
对于一个值的重复运算,存入临时变量中
消除条件跳转
a:对于适合分治预测的数据,测得平均一次循环需要\(4.0\)个时钟周期;对于随机数据,测得平均一次循环需要\(12.8\)个时钟周期。可见,分支预测错误的惩罚为\(2\times (12.8-4.0)=17.6\)个时钟周期。
b:用三元运算符重写,让编译器生成一种基于条件传送的汇编代码。测得不论数据如何,平均一次循环只需要\(4.1\)个时钟周期。
//a.cpp
void minmax1(int *a,int *b,int n)
{
for(int i=1;i<=n;i++)
if(a[i]>b[i])
{
int t=a[i];
a[i]=b[i];
b[i]=t;
}
}
//b.cpp
void minmax2(int *a,int *b,int n)
{
for(int i=1;i<=n;i++)
{
int mi=a[i]<b[i]?a[i]:b[i];
int ma=a[i]<b[i]?b[i]:a[i];
a[i]=mi;
b[i]=ma;
}
}
循环展开
a:平均每个元素需要\(3.65\)个时钟周期。
b:平均每个元素需要\(1.36\)个时钟周期。
这样能够刺激CPU并行。
当展开次数过多时,性能反而会下降,因为寄存器不够用\(\longrightarrow\)寄存器溢出
注意每部分要独立以及处理非展开次数的倍数的部分
//a.cpp
double sum(double *a,int n)
{
double s=0;
for(int i=1;i<=n;i++)
{
s+=a[i];
}
return s;
}
//b.cpp
double sum(double *a,int n)
{
double s0=0,s1=0,s2=0,s3=0;
for(int i=1;i<=n;i+=4)
{
s0+=a[i];
s1+=a[i+1];
s2+=a[i+2];
s3+=a[i+3];
}
return s0+s1+s2+s3;
}
编写缓存友好的代码
空间局部性好
尽量使用步长为\(1\)的访问模式,即访问的内存是连续的。
在遍历高维数组是很重要
时间局部性好
是内存访问的工作集尽量小
在统计整数二进制表示中\(1\)的个数时,分两段查表有时不如分三段好。
避免使用步长为较大的\(2\)的幂的访问模式
避免缓存冲突。
在状压DP、使用高位数组时很重要
解决方法:把数组稍微开大一些
一些数据
类型 | 延迟(周期数) |
---|---|
CPU寄存器 | \(0\) |
TLB | \(0\) |
L1高速缓存 | \(4\) |
L2高速缓存 | \(10\) |
L3高速缓存 | \(50\) |
虚拟内存 | \(200\) |
在某Intel Core i5 CPU上,有这些高速缓存:
高速缓存类型 | 访问时间(周期) | 高速缓存大小 | 相联度 | 块大小 | 组数 |
---|---|---|---|---|---|
L1 I-Cache | \(4\) | \(32\)KB | \(8\) | \(64\)B | \(64\) |
L1 D-Cache | \(4\) | \(32\)KB | \(8\) | \(64\)B | \(64\) |
L2 Cache | 约\(12\) | \(256\)KB | \(4\) | \(64\)B | \(512\) |
L3 Cache | 约\(50\) | \(6\)MB | \(12\) | \(64\)B | \(8192\) |
对于不同的\(n\)和\(d\),反复调用这个程序,具有不同的时空局部性。
容易得知,\(n\)越小,时间局部性越好,\(d\)越小,空间局部性越好。
int sum(int *a,int n,int d)
{
int s=0;
for(int i=0;i<n;i++)
s+=a[i*d];
return s;
}
空间局部性
\(n\)足够大时结果如下
与理论相符
\(d\) | \(1\) | \(2\) | \(3\) | \(4\) | \(8\) | \(16\) | \(32\) | \(64\) |
---|---|---|---|---|---|---|---|---|
周期数 | \(1.50\) | \(2.34\) | \(3.46\) | \(4.73\) | \(9.70\) | \(15.00\) | \(19.76\) | \(20.26\) |
时间局部性
\(n=200\)时结果如下
\(d\) | \(2^{19}\) | \(2^{19}+1\) |
---|---|---|
周期数 | \(159\) | \(1.18\) |
这是为什么呢?
\(200\)个整数,显然能在L1缓存装得下?
对于\(d=2^{19}\),每次内存访问时,地址的后\(19\)位都是一样的。
根据CPU高速缓存的原理,这些地址必然会被映射到同一个组
因此,缓存只有一组,\(159\)周期就是内存访问速度。
p.s.:后\(19\)位一样的是虚拟地址,在映射成物理地址之后,由于操作系统的特性,也至少有后\(12\)位是一样的。