首先聲明,本博文部分內容僅僅適用於ACM競賽,並不適用於NOIP與OI競賽,違規使用可能會遭競賽處理,請慎重使用!遭遇任何情況都與本人無關哈=7=
我也不想搞得那么嚴肅的,但真的有些函數在NOIP與OI競賽中有相關規定不能使用,詳細我也不知道各位要了解請自行去找比賽要求咯,當然在ACM競賽中,沒有限制函數,所以所有內容都適用於ACM競賽。
那么什么是卡常數呢,簡單來說就是你和某神犇算法思路一樣,結果他的AC了,你的TLE,復雜來說就是程序被卡常數,一般指程序雖然漸進復雜度可以接受,但是由於實現/算法本身的時間常數因子較大,使得無法在OI/ACM等算法競賽規定的時限內運行結束。
下面就是介紹各種各樣的非(花)常(里)實(胡)用(哨)的優化方法的,若本文某些地方有錯誤或不明確的地方還請指出。=7=
優化I/O
網上有很多說關於cin和scanf的介紹,以及關閉流輸入等等優化方法,但這些都還是有可能成為卡常數的地方,那么這個時候,我們就可以自己寫輸出輸出函數了。
下面一個簡單的對讀入數字的優化:
1 inline void read(int &sum) { 2 char ch = getchar(); 3 int tf = 0; 4 sum = 0; 5 while((ch < '0' || ch > '9') && (ch != '-')) ch = getchar(); 6 tf = ((ch == '-') && (ch = getchar())); 7 while(ch >= '0' && ch <= '9') sum = sum * 10+ (ch - 48), ch = getchar(); 8 (tf) && (sum =- sum); 9 }
因為getchar()是比scanf和cin快很多的,所以可以用這種方式優化很多,當然也可以寫對其他各種類型輸入的優化。
然后就是進階版優化,cstdio庫里面有一個非常快而且和freopen和fopen完美兼容的函數就是fread,而且是整段讀取,函數原型為:
1 size_t fread(void *buffer,size_t size,size_t count,FILE *stream);
作用:從stream中讀取count個大小為size個字節的數據,放到數組buffer中,返回成功了多少個大小為為size個字節的數據。
所以我們的代碼可以更加優化為:
1 inline char nc() { 2 static char buf[1000000], *p1 = buf, *p2 = buf; 3 return p1 == p2 && (p2 = (p1 = buf) + fread (buf, 1, 1000000, stdin), p1 == p2) ? EOF : *p1++; 4 } 5 6 //#define nc getchar 7 inline void read(int &sum) { 8 char ch = nc(); 9 int tf = 0; 10 sum = 0; 11 while((ch < '0' || ch > '9') && (ch != '-')) ch = nc(); 12 tf = ((ch == '-') && (ch = nc())); 13 while(ch >= '0' && ch <= '9') sum = sum * 10+ (ch - 48), ch = nc(); 14 (tf) && (sum =- sum); 15 }
但要注意,由於這種方法是整段讀取的,這也造就了它兩個巨大的Bug:
- 不能用鍵盤輸入。數據還沒輸入,程序怎么整段讀取。如果你需要在電腦上用鍵盤輸入調試,請把第5行的注釋取消。
- 不能和
scanf
,getchar
等其他讀入方法混合使用。因為fread
是整段讀取的,也就是說所有數據都被讀取了,其他函數根本讀取不到任何東西(只能從你的讀取大小后面開始讀),因此,所有類型的變量讀入都必須自己寫,上面的read
函數只支持int
類型。
下面是測試,摘自LibreOJ,單位為毫秒
# | Language | [0,2) | [0,8) | [0,2^{15})) | [0,2^{31}) | [0,2^{63}) |
---|---|---|---|---|---|---|
fread | G++ 5.4.0 (-O2) | 13 | 13 | 39 | 70 | 111 |
getchar | G++ 5.4.0 (-O2) | 58 | 73 | 137 | 243 | 423 |
cin(關閉同步) | G++ 5.4.0 (-O2) | 161 | 147 | 205 | 270 | 394 |
cin | G++ 5.4.0 (-O2) | 442 | 429 | 706 | 1039 | 1683 |
scanf | G++ 5.4.0 (-O2) | 182 | 175 | 256 | 368 | 574 |
fread
以壓倒性的優勢碾壓了其他所有方法,還可以注意到關流同步的cin比scanf快,關於為什么不使用位運算的問題下面會說。
然后就是輸出的優化,同理,putchar()會比printf快,所以,輸出數字可以優化成:
1 // 優化前輸出1-10000000:4.336秒 2 // 優化后輸出1-10000000:1.897秒 3 void print( int k ){ 4 num = 0; 5 while( k > 0 ) ch[++num] = k % 10, k /= 10; 6 while( num ) 7 putchar( ch[num--]+48 ); 8 putchar( 32 ); 9 10 }
如果輸出負數以及其他,就自己寫一個或者百度啦,我這里就不貼了。其實大多數還是對讀入進行優化,輸出一般用printf就可以了。
位運算
很多人都肯定很喜歡用位運算吧,因為覺得位運算是基於二進制操作,肯定比普通加減乘除快很多,但是真的是所有的位運算操作都比常規操作快么。
乘和除的位運算
1 x << 1; 2 x *= 2;
例如上面這兩句,都是把x乘2,但真的用位運算會快么,其實他們理論上是一樣的,在被g++翻譯成匯編后,兩者的語句都是
1 addl %eax, %eax1
它等價於 x = x + x。所以在這里位運算並沒有任何優化。那么把乘數擴大呢,比如乘10,x *= 10的匯編語言為
1 leal (%eax,%eax,4), %eax 2 addl %eax, %eax
翻譯過來就是
1 x = x + x*4; 2 x = x + x;
而那些喜歡用(x << 3 + x << 1)的人自己斟酌!
但是位運算在某些地方是非常有用的,比如除法,右移的匯編代碼為
1 movl _x, %eax 2 sarl %eax 3 movl %eax, _x 4 movl _x, %eax
而除二的匯編代碼為
1 movl _x, %eax 2 movl %eax, %edx //(del) 3 shrl $31, %edx //(del) 4 addl %edx, %eax //(del) 5 sarl %eax 6 movl %eax, _x 7 movl _x, %eax
可以看到,右移會比除快很多。
%2和&1
這個其實可想而知&1快,還是看下匯編代碼吧,%2的匯編代碼為
1 movl _x, %eax 2 movl $LC0, (%esp) 3 movl %eax, %edx //(del) 4 shrl $31, %edx //(del) 5 addl %edx, %eax //(del) 6 andl $1, %eax 7 subl %edx, %eax //(del) 8 movl %eax, 4(%esp) 9 movl %eax, _x
&1的匯編代碼為
1 movl _x, %eax 2 movl $LC0, (%esp) 3 andl $1, %eax 4 movl %eax, 4(%esp) 5 movl %eax, _x
^和swap
最開始學C語言兩個變量交換都是先學三變量交換法,再學^這種操作,下面是(a ^= b ^= a ^= b)的匯編代碼
1 movl _b, %edx 2 movl _a, %eax 3 xorl %edx, %eax 4 xorl %eax, %edx 5 xorl %edx, %eax 6 movl %eax, _a 7 xorl %eax, %eax 8 movl %edx, _b
再來看看(int t = a;a = b,b = t;)的匯編代碼
1 movl _a, %eax 2 movl _b, %edx 3 movl %eax, _b 4 xorl %eax, %eax 5 movl %edx, _a
誰慢誰快一眼就知道了,以后swap再無Xor。
其他位運算技巧
網上有很多奇奇怪怪的位運算技巧,但有一些真的令人很無語,沒有優化不說,大大降低了代碼可讀性,在我看來,都是些花里胡哨的操作,比如取絕對值(n ^ (n >> 31)) - (n >> 31),取兩個數的最大值b & ((a - b) >> 31) | a & ( ~(a - b) >> 31),取兩個數的最小值a & ((a - b) >> 31) | b & ( ~(a-b) >> 31 )。恕我愚鈍,這些代碼一眼看上去根本不知道在干嘛,還有那個取絕對值的和abs(x),誰快都不用說了。
但是位運算還是有很多好(騷)操作的,例如:
lowbit函數 : x & (-x)
判斷是不是2的冪:x > 0 ? ( x & (x - 1)) == 0 : false
emmm……還有很多,我就不介紹了(我就知道這兩個=7=)
條件判斷優化
acm不可避免會有條件語句,if-else也好,?:也好,switch也好,那么問題來了,最后用哪種呢,讓我們一一道來。
if和?:
網上很多說if比?:慢,但是其實不是這樣的,二者的匯編除了文件名不一樣其他都一模一樣。其實不是?:比if快而是?:比if-else快。
有什么區別嗎?你需要先弄清楚if-else的工作原理。
if就像一個鐵路分叉道口,在CPU底層這種通訊及其不好的地方,在火車開近之前,鬼知道火車要往哪邊開,那怎么辦?猜!
如果猜對了,它直接通過,繼續前行。
如果猜錯了,車頭將停止,倒回去,你將鐵軌扳至反方向,火車重新啟動,駛過道口。
如果是第一種情況,那很好辦,那第二種呢?時間就這么浪過去了,假如你非常不走運,那你的程序就會卡在停止-回滾-熱啟動的過程中。
上面猜的過程就是分支預測。
雖然是猜,但編譯器也不是隨便亂猜,那怎么猜呢?答案是分析之前的運行記錄。假設之前很多次都是true,那這次就猜true,如果最近連續很多次都是false,那這次就猜false。
但這一切都要看你的CPU了,因此,一般把容易成立的條件寫在前面判斷,把不容易成立的條件放在else那里。
但是?:消除了分支預測,因此在布爾表達式的結果近似隨機的時候?:更快,否則就是if更快啦。
分支預測優化
gcc存在內置函數:__builtin_expect(!!(x), tf),他不會改變x的值,僅僅只是減少跳轉次數,當tf為true時表示x非常可能為true,反之同理。
用法就是if(__builtin_expect(!!(x),0)) 或者把0換為1,這樣在if猜的時候就會優先猜x為true或是false,達到優化效果。
switch和if-else
這個東西還是有必要提一下,當switch沒有default的時候,switch會比if-else快,因為他是直接跳轉而不是逐條判斷,但加了default之后,switch也就變成了無腦判斷模式,至於為什么會這樣,各位就自行研究咯=7=
短路運算符
我們知道&&和||是兩個短路運算符,什么叫短路運算符,就是一旦可以確定了表達式的真假值時候,就直接返回真假值了,比如下面代碼
1 int n = 0; 2 3 n && ++n; 4 5 //這里n的值還是0 6 7 !n || ++n; 8 9 //這里n的值還是0
但是上面的兩句代碼等同於什么呢?等於
int n = 0; if(n){ ++n; } if(!(!n)){ ++n; }
利用這個特色(你才特色),我們有些時候就可以不需要在做if的無腦判斷了,也就是
- if(A) B; → (A)&&(B)
- if(A) B; else C; → A&&(B,1)||C
但這些並不是短路運算符的精髓,短路運算符的精髓不僅在於優化時間,更是可以防止程序出錯。
1 double t = rand(); 2 if (t / RAND_MAX < 0.2 && t != 0) 3 printf ("%d", t); 4 5 double t = rand(); 6 if (t != 0 && t / RAND_MAX < 0.2) 7 printf ("%d", t);
這兩種判斷,誰快誰慢。但對於CPU來說很有區別。第一段代碼中的t/RAND_MAX<0.2為true的概率約為 20%,但t!=0為true的概率約為1/RAND_MAX,明顯小於20%
因此,如果把計算一個不含邏輯運算符布爾表達式的計算次數設為 1 次,設計算了 X 次,則對於第 1 段代碼,X 的數學期望為 6/5 次,但對於第二段代碼,X 的數學期望2*(RAND_MAX-1) / RAND_MAX為 ,遠遠大於第一段代碼。
不僅不同位置會優化時間,更是會防止程序錯誤,例如kuangbin搜索專題有題是Catch the Cow,就是搜索,不過判斷走沒走過得判斷vis[n]和n < 1e6,我最最開始寫的vis[n] && n < 1e6,提交上去RE了,看了很久才發現是這里的原因,得先判斷n < 1e6,再做下一步操作。
所以, 遇到A&&B時,優先把可能為false的表達式放在前面。遇到A||B時,優先把可能為true的表達式放在前面。但也不一定是絕對這樣,還得結合題目。
布爾表達式和逗號運算符
很多人喜歡用if(x == true)這種形式,但其實if(x)就行了,在可讀性等方面都沒有變化。而且不要開bool數組,int是最快的(原因暫時不知道)。
逗號運算符若干條語句可以通過逗號運算符合並成一條語句。 例如t=a;a=b;b=t;可以寫成t=a,a=b,b=t;有什么用嗎?它的返回值。
int x=(1,2,3,4,5);
猜一猜,上面的語句執行完后x的值是多少? 答案是 5 沒錯,逗號運算符的返回值就是最后一個的值。而且逗號表達式比分號快很多很多,真的。
卡編譯
C++內聯函數inline
:
由編譯器在編譯時會在主程序中把函數的內容直接展開替換,減少了內存訪問,但是這並不是適用於各種復雜以及遞歸式的函數,復雜函數編譯器會自動忽略inline
1 int max(int a, int b){return a>b?a:b;}//原函數 2 inline int max(int a, int b){return a>b?a:b;}//直接加inline就好了。
CPU寄存器變量register
:
對於一些頻繁使用的變量,可以聲明時加上該關鍵字,運行時可能會把該變量放到CPU寄存器中,只是可能,因為寄存器的空間是有限的,不保證有效。特別是你變量多的時候,一般還是丟到內存里面的。
比較下面兩段程序:
1 register int a=0; 2 for(register int i=1;i<=999999999;i++)a++; 3 4 int a=0; 5 for(int i=1;i<=999999999;i++)a++;
優化:0.2826 second
不優化:1.944 second
卡算法
取模優化
1 //設模數為 mod 2 inline int inc(int x,int v,int mod){x+=v;return x>=mod?x-mod:x;}//代替取模+ 3 inline int dec(int x,int v,int mod){x-=v;return x<0?x+mod:x;}//代替取模-
加法優化
用++i代替i++,后置++需要保存臨時變量以返回之前的值,在 STL 中非常慢。
結構優化
如果要經常調用a[x],b[x],c[x]這樣的數組,把它們寫在同一個結構體里面會變快一些,比如f[x].a, f[x].b, f[x].c
指針比下標快,數組在用方括號時做了一次加法才能取地址!所以在那些計算量超大的數據結構中,你每次都多做了一次加法!!!在 64 位系統下是 long long 相加,效率可想而知。
卡算法
STL優化
STL快但是也包含了很多可能你用不到的東西,所以最快的就是你自己手寫STL=7=,反正我寫不來。
循環展開
1 void Init(int *d, int n){ 2 for(int i = 0; i < n; i++) 3 d[i] = 0; 4 } 5 6 7 void Init(int *d, int n){ 8 int il 9 for(int i = 0; i < n; i+= 4){ //每次迭代處理4個元素 10 d[i] = 0; 11 d[i + 1] = 0; 12 d[i + 2] = 0; 13 d[i + 3] = 0; 14 } 15 for(; i < n; i++)//將剩余未處理的元素再依次初始化 16 d[i] = 0; 17 }
都是同一個操作,但你們覺得誰快呢,用下面的比第一段代碼快了不止一倍,循環展開也許只是表面,在緩存和寄存器允許的情況下一條語句內大量的展開運算會刺激 CPU 並發
- 減少了不直接有助於程序結果的操作的數量,例如循環索引計算和分支條件。
- 提供了一些方法,可以進一步變化代碼,減少整個計算中關鍵路徑上的操作數量。
好像沒什么要講的了呢,網上還有一些很邪門的優化方式,我覺得就沒必要了,能大致知道一些優化流程就行了,比如讀入還有mmap但用這個不是很了解的話可能還會用出事,所以別沒必要那么追求極限了。自己覺得講的還是挺多挺全面的,若是哪里有錯誤或者沒講到的地方還請指出。