一些卡常技巧

  什么?你说这些东西没用?

  那你就大错特错了。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 × 3 的矩阵乘法:边加边取模: 27 27 次取模运算;全部算完再取模: 9 9 次取模运算。

优化高位数组的寻址

  用指针保存上一次使用的地址,直接加偏移。

对于一个值的重复运算,存入临时变量中

消除条件跳转

  a:对于适合分治预测的数据,测得平均一次循环需要4.0个时钟周期;对于随机数据,测得平均一次循环需要 12.8 12.8 个时钟周期。可见,分支预测错误的惩罚为 2×(12.84.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 的访问模式,即访问的内存是连续的。

  在遍历高维数组是很重要

时间局部性好

  是内存访问的工作集尽量小

  在统计整数二进制表示中1的个数时,分两段查表有时不如分三段好。

避免使用步长为较大的 2 2 的幂的访问模式

  避免缓存冲突。

  在状压DP、使用高位数组时很重要

  解决方法:把数组稍微开大一些

一些数据

类型 延迟(周期数)
CPU寄存器 0 TLB 0 0
L1高速缓存 4 L2高速缓存 10 10 L3高速缓存 50 50 虚拟内存 200 200

  在某Intel Core i5 CPU上,有这些高速缓存:

高速缓存类型访问时间(周期)高速缓存大小相联度块大小组数
L1 I-Cache 4 4 32KB 8 8 64B 64 64
L1 D-Cache 4 4 32KB 8 8 64B 64 64
L2 Cache 12 12 256 256 KB 4 4 64B 512 512
L3 Cache 50 50 6 6 MB 12 64 64 B 8192 8192

  对于不同的 n n d,反复调用这个程序,具有不同的时空局部性。

  容易得知, n 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 n 足够大时结果如下

  与理论相符

d 1 1 2 3 3 4 8 8 16 32 32 64 64 周期数 1.50 1.50 2.34 2.34 3.46 3.46 4.73 4.73 9.70 9.70 15.00 15.00 19.76 19.76 20.26 20.26

时间局部性

   n=200 n = 200 时结果如下

d d 219 219+1 2 19 + 1
周期数 159 159 1.18 1.18

  这是为什么呢?

   200 200 个整数,显然能在L1缓存装得下?

  对于 d=219 d = 2 19 ,每次内存访问时,地址的后 19 19 位都是一样的。

  根据CPU高速缓存的原理,这些地址必然会被映射到同一个组

  因此,缓存只有一组, 159 159 周期就是内存访问速度。

  p.s.:后 19 19 位一样的是虚拟地址,在映射成物理地址之后,由于操作系统的特性,也至少有后 12 12 位是一样的。

  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值