什么?你说这些东西没用?
那你就大错特错了。WC考过的东西怎么可能没用
开O2之后FFT会比不开快几倍
不开O2:NTT比FFT快
开O2:FFT比NTT快
常数尽量声明成常量
有一道NTT的题,模数声明成变量跑了 1166 1166 ms,模数声明成常量跑了不到 300 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 1 个时钟周期,乘法运算只要个时钟周期,而除法和取模运算要几到几十个时钟周期。
3×3 3 × 3 的矩阵乘法:边加边取模: 27 27 次取模运算;全部算完再取模: 9 9 次取模运算。
优化高位数组的寻址
用指针保存上一次使用的地址,直接加偏移。
对于一个值的重复运算,存入临时变量中
消除条件跳转
a:对于适合分治预测的数据,测得平均一次循环需要个时钟周期;对于随机数据,测得平均一次循环需要 12.8 12.8 个时钟周期。可见,分支预测错误的惩罚为 2×(12.8−4.0)=17.6 2 × ( 12.8 − 4.0 ) = 17.6 个时钟周期。
b:用三元运算符重写,让编译器生成一种基于条件传送的汇编代码。测得不论数据如何,平均一次循环只需要 4.1 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 3.65 个时钟周期。
b:平均每个元素需要 1.36 1.36 个时钟周期。
这样能够刺激CPU并行。
当展开次数过多时,性能反而会下降,因为寄存器不够用 ⟶ ⟶ 寄存器溢出
注意每部分要独立以及处理非展开次数的倍数的部分
//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 2 的幂的访问模式
避免缓存冲突。
在状压DP、使用高位数组时很重要
解决方法:把数组稍微开大一些