一些卡常技巧

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

  那你就大错特错了。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\)位是一样的。

posted @ 2018-03-05 21:25  ywwyww  阅读(2266)  评论(2编辑  收藏  举报