• 一些卡常技巧


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

      那你就大错特错了。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
    ",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
    ",a);
    	return 0;
    }
    

    能用位运算尽量用位运算

      当然,编译器大多数情况下会帮你优化掉。

    少用除法和取模

      加法运算只要(1)个时钟周期,乘法运算只要(3)个时钟周期,而除法和取模运算要几到几十个时钟周期。

      (3 imes 3)的矩阵乘法:边加边取模:(27)次取模运算;全部算完再取模:(9)次取模运算。

    优化高位数组的寻址

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

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

    消除条件跳转

      a:对于适合分治预测的数据,测得平均一次循环需要(4.0)个时钟周期;对于随机数据,测得平均一次循环需要(12.8)个时钟周期。可见,分支预测错误的惩罚为(2 imes (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)位是一样的。

  • 相关阅读:
    【强化学习】python 实现 q-learning 例二
    【强化学习】python 实现 q-learning 例一
    python棋类游戏编写入门
    推荐几个表格工具
    【Java并发编程实战】----- AQS(四):CLH同步队列
    【Java并发编程实战】----- AQS(三):阻塞、唤醒:LockSupport
    【Java并发编程实战】----- AQS(二):获取锁、释放锁
    【Java并发编程实战】----- AQS(一):简介
    【Java并发编程实战】-----“J.U.C”:CLH队列锁
    【Java并发编程实战】-----“J.U.C”:CAS操作
  • 原文地址:https://www.cnblogs.com/ywwyww/p/8511463.html
Copyright © 2020-2023  润新知