ACM卡常數(各類玄學優化)

首先聲明,本博文部份內容僅僅適用於ACM競賽,並不適用於NOIP與OI競賽,違規使用可能會遭競賽處理,請慎重使用!遭遇任何狀況都與本人無關哈=7=html

我也不想搞得那麼嚴肅的,但真的有些函數在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快不少的,因此能夠用這種方式優化不少,固然也能夠寫對其餘各類類型輸入的優化。app

而後就是進階版優化,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:

  1. 不能用鍵盤輸入。數據還沒輸入,程序怎麼整段讀取。若是你須要在電腦上用鍵盤輸入調試,請把第5行的註釋取消。
  2. 不能和scanfgetchar等其餘讀入方法混合使用。由於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但用這個不是很瞭解的話可能還會用出事,因此別不必那麼追求極限了。本身以爲講的仍是挺多挺全面的,如果哪裏有錯誤或者沒講到的地方還請指出。

相關文章
相關標籤/搜索