什麼?你說這些東西沒用?數組
那你就大錯特錯了。WC考過的東西怎麼可能沒用緩存
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:NTT比FFT快
開O2:FFT比NTT快spa
有一道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\)個時鐘週期,而除法和取模運算要幾到幾十個時鐘週期。code
\(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\)的個數時,分兩段查表有時不如分三段好。
避免緩存衝突。
在狀壓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\)位是同樣的。