一些卡常技巧
什麽?你說這些東西沒用?
那你就大錯特錯了。WC考過的東西怎麽可能沒用
開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\)
優化高位數組的尋址
用指針保存上一次使用的地址,直接加偏移。
對於一個值的重復運算,存入臨時變量中
消除條件跳轉
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\)位是一樣的。
一些卡常技巧