如何優(yōu)化程序

本文為CSAPP第五章學(xué)習(xí)筆記纫骑。

編寫高效的程序需要:

  1. 選擇合適的數(shù)據(jù)結(jié)構(gòu)和算法
  2. 編寫出編譯器能夠有效優(yōu)化以轉(zhuǎn)換成高效可執(zhí)行代碼的源代碼
  3. 對(duì)于計(jì)算量較大的任務(wù)谓厘,可以將其分解為若干小的代碼段,然后并行計(jì)算

優(yōu)化代碼:

  1. 減少不必要的內(nèi)容灵巧,讓代碼盡可能簡(jiǎn)單的執(zhí)行期望的工作髓需。如消除不必要的函數(shù)調(diào)用缝裤、條件測(cè)試和存儲(chǔ)器引用。
  2. 利用處理器提供的指令集并行能力铜靶,同時(shí)執(zhí)行多條指令叔遂。根據(jù)代碼的各項(xiàng)操作的時(shí)序特性做出合理安排,以避免不必要的等待。

在優(yōu)化代碼的時(shí)候已艰,要保證代碼的簡(jiǎn)潔和可讀性痊末,因?yàn)榇a終歸需要維護(hù)和擴(kuò)展。

1 減少存儲(chǔ)器調(diào)用

考慮如下兩個(gè)函數(shù):

void twiddle1(int *xp, int *yp)
{
    *xp += *yp;
    *xp += *yp; 
}

void twiddle2(int *xp, int *yp)
{
    *xp += 2* *yp;
}

twiddlw1()twiddle2()有著相似的計(jì)算行為哩掺。但是舌胶,twiddle2()只有三次寄存器調(diào)用:*xp的讀取,*yp的讀取以及*xp的寫入疮丛。twiddle1()則有六次寄存器調(diào)用幔嫂,實(shí)際計(jì)算更為麻煩。

twiddlw1()twiddle2()也不完全相同誊薄。試考慮履恩,*xp = *yptwiddlw1()*xp實(shí)際等于4* *xp呢蔫,而twiddlw2()*xp實(shí)際等于3* *xp切心。這種兩個(gè)指針指向統(tǒng)一存儲(chǔ)器的情況,也稱作存儲(chǔ)器別名使用片吊。

2 減少函數(shù)調(diào)用

考慮如下兩個(gè)函數(shù):

void cycle1(int *xp, int *yp)
{
    return f() + f() + f() + f();
}

void cycle2(int *xp, int *yp)
{
    return 4*f();
}

cycle1()函數(shù)調(diào)用了f()函數(shù)四次绽昏,需要在寄存器執(zhí)行4次 建立棧幀 -> 計(jì)算f() -> 恢復(fù)棧幀 過程。而cycle2()函數(shù)只需要一次俏脊。

當(dāng)然全谤,作為特殊情況,需要考慮f()會(huì)改變?nèi)肿兞康目赡芤丁H绻_實(shí)改變认然,那么cycle1()函數(shù)就會(huì)改變?nèi)肿兞?次,而cycle2()函數(shù)只會(huì)改變1次漫萄。

3 每元素周期數(shù)CPE

考慮一個(gè)計(jì)算向量的前置和(prefix sum)的過程卷员。

向量P=(a1, a2, a3, .., ai,..., an)的前置和p(i)的計(jì)算過程可寫為:

p(1) = a1;
p(i) = p(i-1) + ai; # i > 1

代碼1

void combine(vec_pointer ptr, data_t *dest)
{
    long int i;                     
    *dest = 0;
    for (i = 0; i < length(ptr); i++) { /* length(ptr)返回得是ptr所指向的向量的所含元素個(gè)數(shù)  */
        data_t val;                     /* 新建一個(gè)內(nèi)存空間 */
        get_vec_element(ptr, i, &val);  /* 將向量指針ptr所指的向量的第i項(xiàng)存入val */
        *dest = *dest + val;            /* *dest累加val,求的前置和 */
    }
}

代碼1使用for循環(huán)迭代計(jì)算前置和腾务。無論ptr所指向量有多大毕骡,每次調(diào)用函數(shù)都會(huì)執(zhí)行建立棧幀恢復(fù)棧幀,這一段代碼的耗時(shí)是一個(gè)定值S岩瘦。隨著循環(huán)次數(shù)n的變化未巫,總的耗時(shí)T=S + nL,其中L為for循環(huán)的單位循環(huán)執(zhí)行時(shí)間担钮,在本書中又稱作每元素周期數(shù)CPE*橱赠。

4 消除循環(huán)的低效率——代碼移動(dòng)

代碼1在for循環(huán)中尤仍,每次循環(huán)都會(huì)調(diào)用length()獲取向量的所含元素個(gè)數(shù)箫津。然而這個(gè)值通常都是一個(gè)定值,如果將其存儲(chǔ)在一個(gè)局部變量中,降低調(diào)用頻率苏遥,可以有效改善代碼運(yùn)行效率饼拍。

代碼2

void combine(vec_pointer ptr, data_t *dest)
{
    long int i;                     
    *dest = 0;
    long int v_length = length(ptr);   /* 將length(ptr)返回值存儲(chǔ)在局部變量中 */
    for (i = 0; i < v_length; i++) { 
        data_t val;                     
        get_vec_element(ptr, i, &val);  
        *dest = *dest + val;            
    }
}

5 減少過程調(diào)用

代碼2的for循環(huán)中,每次都要掉用get_vec_element()來獲得第i位元素田炭,也同樣代價(jià)巨大师抄。一個(gè)合理的替代方案是:直接獲取向量,存入局部變量中教硫,然后按需調(diào)用叨吮。

代碼3

/*
 * 已知vec_pointer結(jié)構(gòu)體的定義 
 */
typedef struct  {
    long int len;
    data_t *data;
} vec_rec, vec_pointer;

/*
 * 新建函數(shù)
 */
data_t *get_vec_start(vec_pointer ptr)
{
    return ptr->data;                   /* 直接獲得ptr所指向量的數(shù)據(jù)部分的頭段指針 */
}

void combine(vec_pointer ptr, data_t *dest)
{
    long int i;                     
    *dest = 0;
    long int v_length = length(ptr);    
    data_t *data = get_vec_start(ptr);  /* 將ptr數(shù)據(jù)存入數(shù)組 */
    for (i = 0; i < v_length; i++) { 
        *dest = *dest + data[i];        
    }
}

6 消除不必要的存儲(chǔ)器引用

下面是代碼3中for循環(huán)的匯編代碼:

/*
 * code3: data_t = float
 * i 位于 %rdx, data 在 %rax, dest 在 %rbp, 越界標(biāo)志 limit 在 %r12
 */

.L498:
    movss (%rbp), %xmm0           /* 取出dest,存入 %xmm0 */
    mulss (%rax, %rdx, 4), %xmm0  /* 取出data[i], 并與dest相乘 */
    movss %xmm0, (%rbp)           /* 將結(jié)果存入dest */
    addq  $1, %rdx                /* i加一 */
    cmpq  %rdx, %r12              /* 比較i是否越界 */
    jg    .L498                   /* 如果沒有越界瞬矩,就再次循環(huán) */

可以看出代碼3的for循環(huán)中茶鉴,每次計(jì)算加法都會(huì)先引用寄存器中*dest所指向的空間,然后加和景用,最后將計(jì)算結(jié)果存入寄存器涵叮。但比較浪費(fèi)的是,每次讀取的值都是上次的計(jì)算結(jié)果伞插。

合理的解決辦法是割粮,將累加值存入局部變量中,當(dāng)計(jì)算結(jié)束后再把最終結(jié)果存入寄存器媚污。

代碼4

void combine(vec_pointer ptr, data_t *dest)
{
    long int i; 
    long int v_length = length(ptr);    
    data_t *data = get_vec_start(ptr);  
    data_t acc = 0;                     /* 局部變量存儲(chǔ)累加值 */
    for (i = 0; i < v_length; i++) { 
        acc = acc + data[i];            /* 用累加器acc累加data[i]舀瓢,求前置和 */
    }
    *dest = acc;                        /* 將結(jié)果存入寄存器 */
}

其對(duì)應(yīng)匯編代碼

/*
 * code4: data_t = float
 * i 位于 %rdx, data 在 %rax, 越界標(biāo)志 limit 在 %rbp, acc 在 %xmm0
 */

.L488:
    mulss (%rax, %rdx, 4), %xmm0  /* 取出data[i], 并與acc相乘 */
    addq  $1, %rdx                /* i加一 */
    cmpq  %rdx, %rbp              /* 比較i是否越界 */
    jg    .L488                   /* 如果沒有越界,就再次循環(huán) */

7 循環(huán)展開

至此耗美,for循環(huán)內(nèi)部代碼已經(jīng)足夠簡(jiǎn)潔氢伟。然而,循環(huán)本身也存在開銷幽歼,如果能夠在保證計(jì)算結(jié)果足夠精準(zhǔn)的情況下朵锣,減少循環(huán)次數(shù),也能產(chǎn)生明顯的改善效果甸私。

循環(huán)展開诚些,就是一種程序變換,通過增加每次迭代計(jì)算的元素的數(shù)量皇型,來減少循環(huán)的迭代次數(shù)诬烹。循環(huán)展開從兩方面改善了程序的性能:

  1. 減少了不直接有助于程序結(jié)果的操作的數(shù)量,如循環(huán)索引計(jì)算弃鸦、條件分支绞吁;
  2. 可以進(jìn)一步變化代碼,減少整個(gè)計(jì)算的關(guān)鍵路徑上的操作數(shù)量唬格。

關(guān)鍵路徑:在循環(huán)的反復(fù)執(zhí)行過程中形成的數(shù)據(jù)相關(guān)鏈家破。

代碼5

void combine(vec_pointer ptr, data_t *dest)
{
    long int i; 
    long int v_length = length(ptr);         
    long int limit = v_length - 1;      
    data_t *data = get_vec_start(ptr);       
    data_t acc = 0;                          
    
    /*循環(huán)1*/
    for (i = 0; i < limit; i+=2) {           /* 步進(jìn)為2 */
        acc = (acc + data[i]) + data[i+1];   /* acc累加下兩個(gè)data[i]颜说,求前置和 */
    }
    
    /*循環(huán)2*/
    for (; i < length; i++) {                /* 累加剩余元素 */
        acc = acc + data[i];
    }
    *dest = acc;                             
}

觀察代碼5,有兩個(gè)for循環(huán):

  1. 對(duì)于第一個(gè)循環(huán)汰聋,要保證循環(huán)不會(huì)越界(特別是data[i+1])门粪;
  2. 要保證當(dāng)循環(huán)索引i滿足i<n-1實(shí)才會(huì)執(zhí)行循環(huán),因此最大索引i+1滿足i+1<(n-1)+1=n烹困。

8 提高并行性

觀察代碼4的循環(huán)中的計(jì)算:每次計(jì)算acc之前必須等前一循環(huán)的acc計(jì)算完成后才能繼續(xù)玄妈。

同樣代碼5中循環(huán)1的計(jì)算行:每次計(jì)算acc之前先算acc + data[i],然后計(jì)算+ data[i+1]髓梅,同樣需要等待前已循環(huán)的acc計(jì)算完畢拟蜻。

也就是說,acc的計(jì)算構(gòu)成了一個(gè)單序列的計(jì)算流程枯饿,也就是一條關(guān)鍵路徑瞭郑。如果能夠?qū)⑦@個(gè)流程拆分,就可以利用CPU的亂序特性鸭你,同時(shí)計(jì)算屈张,提高效率。

一個(gè)可行的方法就是:先分別計(jì)算奇數(shù)位元素袱巨、偶數(shù)位元素的和阁谆,然后將兩者加和。

代碼6

void combine(vec_pointer ptr, data_t *dest)
{
    long int i; 
    long int v_length = length(ptr);         
    long int limit = v_length - 1;      
    data_t *data = get_vec_start(ptr);       
    data_t acc1 = 0;
    data_t acc2 = 0;                          
    
    /*循環(huán)1*/
    for (i = 0; i < limit; i+=2) {           
        acc1 = acc1 + data[i];     /* 僅奇數(shù)位元素求前置和*/
        acc2 = acc2 + data[i+1];   /* 僅偶數(shù)位元素求前置和 */
    }
    
    /*循環(huán)2*/
    for (; i < length; i++) {      /* 累加剩余元素 */
        acc1 = acc1 + data[i];
    }
    *dest = acc1 + acc2;           /* 兩個(gè)累加器求和 */                            
}

代碼6中有兩個(gè)關(guān)鍵路徑愉老。

9 重新結(jié)合變換

考慮對(duì)代碼5中循環(huán)1的計(jì)算行:

acc = (acc + data[i]) + data[i+1];

做出變換:

acc = acc + (data[i] + data[i+1]);

盡管只是對(duì)計(jì)算式更改了括號(hào)的位置场绿,但這對(duì)計(jì)算性能有了很大的提高。前式的第一次加法acc + data[i]前仍然需要等待前一循環(huán)acc計(jì)算完畢嫉入,而后式的第一次加法data[i] + data[i+1]則無此要求焰盗。利用CPU的亂序特性,可以在對(duì)于后式計(jì)算:可以在計(jì)算前一循環(huán)acc的同時(shí)咒林,去計(jì)算后一循環(huán)的data[i] + data[i+1]熬拒,從而提高了效率。

代碼7

void combine(vec_pointer ptr, data_t *dest)
{
    long int i; 
    long int v_length = length(ptr);         
    long int limit = v_length - 1;      
    data_t *data = get_vec_start(ptr);       
    data_t acc = 0;                          
    
    /*循環(huán)1*/
    for (i = 0; i < limit; i+=2) {          
        acc = acc + (data[i] + data[i+1]);   /* 重新結(jié)合變換 */
    }
    
    /*循環(huán)2*/
    for (; i < length; i++) {                
        acc = acc + data[i];
    }
    *dest = acc;                             
}

10 一些問題

  1. 循環(huán)的并行度不能無限提高垫竞。一旦平行度超過了可用的寄存器數(shù)量澎粟,編譯器就會(huì)把多余的變量存入棧內(nèi)——從而性能巨減。

  2. 現(xiàn)代CPU都有預(yù)測(cè)分支并提前執(zhí)行的能力欢瞪。但是活烙,一旦CPU預(yù)測(cè)錯(cuò)誤,就會(huì)造成巨大的性能損失遣鼓。

    • 首先啸盏,不要過多的關(guān)注可預(yù)測(cè)的分支(P361);
    • 其次,對(duì)于難以預(yù)測(cè)的情況骑祟,盡可能使用條件數(shù)據(jù)傳送回懦,而不是條件控制轉(zhuǎn)移气笙。

v = test-expr ? then-expr : else-expr;的匯編代碼可能產(chǎn)生下面兩種結(jié)果:

/* 
 *條件數(shù)據(jù)傳送 
 */
vt = then-expr;
v = else-expr;
t = test-expr;
if (t) v = vt;        
/* 
 * 條件控制轉(zhuǎn)移 
 */
      if (!test-expr)
          goto false;
      v = then-expr;
      goto done;
  false:
      v = else-expr;
done:
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市粉怕,隨后出現(xiàn)的幾起案子健民,更是在濱河造成了極大的恐慌抒巢,老刑警劉巖贫贝,帶你破解...
    沈念sama閱讀 216,919評(píng)論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異蛉谜,居然都是意外死亡稚晚,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,567評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門型诚,熙熙樓的掌柜王于貴愁眉苦臉地迎上來客燕,“玉大人,你說我怎么就攤上這事狰贯∫泊辏” “怎么了?”我有些...
    開封第一講書人閱讀 163,316評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵涵紊,是天一觀的道長(zhǎng)傍妒。 經(jīng)常有香客問我,道長(zhǎng)摸柄,這世上最難降的妖魔是什么颤练? 我笑而不...
    開封第一講書人閱讀 58,294評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮驱负,結(jié)果婚禮上嗦玖,老公的妹妹穿的比我還像新娘。我一直安慰自己跃脊,他們只是感情好宇挫,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,318評(píng)論 6 390
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著酪术,像睡著了一般捞稿。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上拼缝,一...
    開封第一講書人閱讀 51,245評(píng)論 1 299
  • 那天娱局,我揣著相機(jī)與錄音,去河邊找鬼咧七。 笑死衰齐,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的继阻。 我是一名探鬼主播耻涛,決...
    沈念sama閱讀 40,120評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼废酷,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了抹缕?” 一聲冷哼從身側(cè)響起澈蟆,我...
    開封第一講書人閱讀 38,964評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎卓研,沒想到半個(gè)月后趴俘,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,376評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡奏赘,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,592評(píng)論 2 333
  • 正文 我和宋清朗相戀三年寥闪,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片磨淌。...
    茶點(diǎn)故事閱讀 39,764評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡疲憋,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出梁只,到底是詐尸還是另有隱情缚柳,我是刑警寧澤,帶...
    沈念sama閱讀 35,460評(píng)論 5 344
  • 正文 年R本政府宣布搪锣,位于F島的核電站秋忙,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏淤翔。R本人自食惡果不足惜翰绊,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,070評(píng)論 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望旁壮。 院中可真熱鬧监嗜,春花似錦、人聲如沸抡谐。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,697評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽麦撵。三九已至刽肠,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間免胃,已是汗流浹背音五。 一陣腳步聲響...
    開封第一講書人閱讀 32,846評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留羔沙,地道東北人躺涝。 一個(gè)月前我還...
    沈念sama閱讀 47,819評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像扼雏,于是被迫代替她去往敵國(guó)和親坚嗜。 傳聞我的和親對(duì)象是個(gè)殘疾皇子夯膀,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,665評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容