天天看點

cpp程式優化 嵌入式C/C++代碼優化 C/C++代碼優化具體方案cpp程式優化

cpp程式優化

博文末尾支援二維碼贊賞哦 _

本文github

C++程式設計優化——讓你的代碼飛起來 RGB格式的彩色圖像先轉換成黑白圖像

C/C++代碼優化具體方案

c++ 性能優化政策

1.關于繼承:盡量少使用多重繼承
    不可否認良好的抽象設計可以讓程式更清晰,代碼更看起來更好,但是她也是有損失的,在繼承體系中子類的建立會調用父類的構造函數,
    銷毀時會調用父類的析構函數,這種消耗會随着繼承的深度直線上升,是以不要過度的抽象和繼承,
    更為嚴重的是當多重繼承中并且有虛函數的存在時情況更為複雜,的确,這些問題涉及開銷,但是多重繼承減少了編碼的負擔,
    同時也讓問題的解決方案更加簡潔,這當然要付出一些代價。總之,與n個基類的多重繼承層次相關的額外虛函數表有n-1個。
    派生類和最左邊的非虛基類共享同一個虛函數表。是以,帶有2個基類的多重繼承層次,
    有1個(2-1=1)基類的虛函數表和1個派生類的虛函數表(最左邊的基類與派生類共享該虛函數表),
    總共有2個虛函數表,如果有虛繼承的存在,會進一步增長這個過程,它是有額外的開銷的。

2.對象的複合:
    對象的複合和繼承很相似,當一個對象包含其他對象構造時也會引起額外的構造。
    關于這點可能會有很多人不解,認為這是不可避免的,舉個例子,比如你的類A中包含了類B非指針和引用對象,
    那麼在你構造對象a的時候會自動調用b的無參構造函數,即使你還沒有用到她,用指針代替就沒有這種消耗,
    另外如果你的一個對象中用到數組和字元串,你是選擇string和vector還是char* 和 c系的數組呢,
    如果沒有用到c++stl庫提供的相關的進階用法,建議選擇後者。

3.構造函數:
    盡量用 參數清單 初始化 代替 參數,避免值傳遞初始化。

4.變量延時定義:
    從c系轉過來的仍保留着c的習慣,在函數第一行先把所有用到的變量都定義好,
    但是c是沒有運作時的消耗的,對于c++時不一樣的,對于c++對象的構造和銷毀時有消耗的,
    如果有大量的對象隻在某個if條件的一個分支中出現,那就會有50%的情況這些消耗是可以避免的。
    對于這點在一個類中也是一樣的,如果成員中有成員隻在某個時刻能用,就用指針代替,
    在構造對象時初始化成空指針,避免構造時調用他的構造函數。

5.虛函數:
    虛函數的底層實作是通過一個 虛函數表 來實作的,是以有虛函數的類構造時必須先初始化虛函數表,
    函數調用時也必須先找到虛函數表,然後通過指針偏移找到相應的函數,通常情況下調用虛函數是沒有運作時消耗的,
    但是根據編譯器的實作不同,在調用虛函數時,有些調用可能導緻增加虛函數表大小的額外開銷,
    或者隻有那些需要調整 this指針 的調用才會發生額外的運作開銷,但不會增加虛函數表的大小,
    在多重繼承 和 虛基類的時候這種消耗會顯著增加,關于繼承已經提過,是以避免濫用虛函數和虛繼承,
    有時候可以用模版設計來代替虛繼承,把運作時的消耗提前到編譯期。
           

虛函數表

6.傳回值優化: 
  雖然c++編譯器會選擇性的進行 RVO(return value optimization) 優化,
  但是不是強制的,當函數有多個傳回語句并且傳回不通名稱的對象,
  函數過于複雜,傳回對象沒有定義拷貝構造函數時,rvo優化是不會執行的,
  是以當函數傳回一個很大的對象時在不确定rvo優化會執行時,盡量避免值傳遞。

7.變量的定義:在定義變量時 盡量避免 類型的不比對 造成臨時變量的産生。

8.記憶體管理:記憶體池
  c++記憶體管理的大權由我們自己掌握,對于項目中要 頻繁申請和釋放的對象 建議用簡單的記憶體池來管理,
  可以大大的降低頻繁申請和釋放記憶體帶來的消耗。

9.善用内聯:
  内聯函數不僅僅是簡單的函數調用似的優化,他還有一個最大的優點就是,
  可以讓編譯期進行進行邊界代碼的運作環境優化,
  内聯把代碼拷貝到執行環境處避免了函數調用帶來的消耗,
  并且編譯期可以進行正常的編譯優化,而函數調用是不能實作的。

10.stl :
  記住一點stl不是唯一的選擇,有時候也不是最好的選擇,合理選擇stl善用stl算法。


11 緩存:對于多次使用的計算結果及時緩存,避免重複計算。

12 延時計算:對于不關心計算結果的計算過程盡量延時執行或者異步去執行。

13 多線程:無鎖化程式設計
   盡可能的使用無鎖式多線程開發,鎖是一個非常消耗性能的東西,
   保證資料同步的手段有很多,voalite,原子操作都可已實作,
   盡量通過一些技巧使用這些手段避免鎖 的使用,如果迫不得已要使用鎖,
   盡量減少鎖的消耗,比如降低鎖的粒度,使用性能更高的鎖等等。
           

線程池 實作代碼

算法優化之c++多線程優化:思考與總結

14 std::move操作: 
   當不得不進行 深拷貝時,如果 深拷貝資料源 在拷貝後就不在使用,盡可能的用move操作代替,
   或者在參數傳遞時 用move操作代替 臨時的 實參變量。

15 cpu緩存:合理的利用cpu cache緩存 可以極大的提高代碼的運作效率(
   例如:數組中以 每列周遊 和 每行周遊的效率的不同), 
   當然多線程環境下也要考慮cpu cache帶來的影響。

16 記憶體對齊:
   在進行網絡程式設計時,最好對網絡中傳送的資料快進行記憶體補齊,
   通常是8位元組對其,提高cpu通路記憶體效率,進而提高資料讀寫速度。

17 函數參數:
   用const引用 代替 值傳遞,如果函數參數過多,
   可以用對象 來 打包參數,減少參數過多帶來的性能消耗。

18 算法: 盡可能的優化你的算法。

19 關于智能指針:必須用
   對于智能指針我的選擇是 必須用,它可以大大降低程式的crash頻率,
   但是智能指針的和普通指針相比是有額外的消耗的,
   她的底層是一個 原子操作 來 統計引用數 和 一個普通指針,
   雖然原子操作和鎖相比性能高了不少但是和普通的加減操作還是慢了不少,
   智能指針 的大小為16個位元組,而 普通指針 的大小隻有4個位元組,
   拷貝的成本也不一樣,是以在使用正确的情況下可以使用 智能指針的引用 來減少拷貝的消耗(
   注意這裡的前提是正确的使用引用,不要引用以一個即将被銷毀的變量)。

20 記憶體池:
   對于需要 頻繁申請和釋放 的記憶體對象,如果可以重複利用對象的記憶體,
   強烈建議通過 記憶體池 或者 重載對象的 new操作符 或者 重載對象的 placement new操作符 
   來減少頻繁的申請和釋放記憶體,進而減少申請和釋放記憶體的消耗和記憶體碎片的産生。

21 其他優化方案:位運算代替乘除法,字首運算符代替字尾運算等等。
           

什麼是并行優化?

并行優化是代碼優化的基本方法,從大到小一共可以分成三級:
   異步架構;任務并行;資料并行。
在實際工作中,
第一步一般是先設計 異步架構,包括 異步處理任務 以及 異步任務 的 異構化 等;
第二步一般是做     資料并行優化(SIMD),利用CPU的 向量指令 來對 多條資料并行處理;
這兩步是 代碼 優化的重心,一般做完這兩步,系統性能會有明顯的提升。

今天要讨論的是第三步,for循環的并行優化。
與前兩者不同的是,for循環往往是處理同一類任務,且通常會涉及到對同一個變量的讀寫,
是以異步是不能用,而且for循環中往往包含 多種結構 比如 邏輯判斷 和 算術過程等,
是以通常也很 難用資料并行的方式來優化,那麼怎麼對for循環進行優化呢?
           
// 例1 記憶體操作,申請一段記憶體空間然後釋放
void testfuc(int num){
    int a = 0;
    for (int i = 0; i != num; i++) {
        int *b = new int[10]();
        delete [] b;
    }
           
假設num = 1e7;一千萬次記憶體操作在我的機器上(2.6 GHz Intel Core i5雙核)運作耗時在900ms左右。
為了對這個for循環進行優化,首先将for循環拆分成若幹部分,比如兩部分:
           
void testfuc(int num){
    int a = 0;
    int num2 = num >> 1;
    // 前半部分
    for (int i = 0; i != num2; i++) {
        int *b = new int[10]();
        delete [] b;
    }
    // 後半部分
    for (int i = num2; i != num; i++) {
        int *b = new int[10]();
        delete [] b;
    }
}
           
然後使用c11的 future + async來啟動兩個異步任務分别處理一個子循環:
           
// https://blog.csdn.net/u011726005/article/details/78266706
//--------------------------------------------------------------------------------
// 3.std::future可以用來擷取異步任務的結果,是以可以把它當成一種簡單的線程間同步的手段。
// std::future通常由某個Provider建立,你可以把Provider想象成一個異步任務的提供者,
// Provider在某個線程中設定共享狀态的值,與該共享狀态相關聯的std::future對象調用get(通常在另外一個線程中擷取該值,
// 如果共享狀态的标志不為ready,則調用std::future::get會阻塞目前的調用者,直到Provider設定了共享狀态的值
//(此時共享狀态的标志變為ready),std::future::get傳回異步任務的值或異常(如果發生了異常)。
 
// 一個有效的std::future 對象通常由以下三種 Provider 建立,并和某個共享狀态相關聯,分别是
// std::async 函數,std::promise::get_future。std::packaged_task::get_future。
 
// 在一個有效的 future 對象上調用get會阻塞目前的調用者,直到Provider設定了共享狀态的值或異常。
 
// 4.c++11還提供了異步接口std::async,通過這個異步接口可以很友善的擷取線程函數的執行結果。
// std::async會自動建立一個線程去調用線程函數,它傳回一個std::future,這個future中存儲了線程函數傳回的結果,
// 當我們需要線程函數的結果時,直接從future中擷取。

void testfuc2(int num){
    int a = 0;
    int num2 = num/2;
    future<void> ft1 = async(std::launch::async, [&]{ // 封裝為lambda函數 後傳入
        for (int i = 0; i != num2; i++) {
            int *b = new int[10]();
            delete [] b;
        }
    });
    
    future<void> ft2 = async(std::launch::async, [&]{ // 封裝為lambda函數 後傳入
        for (int i = num2; i != num; i++) {
            int *b = new int[10]();
            delete [] b;
        }
    });
    
    ft1.wait();
    ft2.wait();
}

           
将testfunc1和testfunc2放在一起測試,運作結果如下:
  time1 = 992.360000
  time2 = 475.182000


顯然testfunc2要明顯比testfunc1快,在本次運作結果中,時間少了一半,
但是這個時間不一定每次都是一半,由于線程切換和CPU狀态的影響,
testfunc2的時間會比testfunc1節省40%-50%。
           
// 複雜計算 優化後函數為:

void testfuc2(int num){
    int num2 = num/2;
    future<void> ft1 = async(std::launch::async, [&]{
        for (int i = 0; i != num2; i++) {
            b = cos(tan(i));
        }
    });
    
    future<void> ft2 = async(std::launch::async, [&]{
        for (int j = num2; j != num; j++) {
            c = cos(tan(j));
        }
    });
    
    ft1.wait();
    ft2.wait();
}

           
運作結果為:

time1 = 806.438000
time2 = 407.875000 
           

嵌入式C/C++代碼優化

1.引言
    計算機技術和資訊技術的高速發展的今天,計算機和計算機技術大量應用在人們的日常生活中,
    嵌入式計算機也得到了廣泛的應用。 
    嵌入式計算機是指完成一種或多種特定功能的計算機系統,是軟硬體的緊密結合體。
    具有軟體代碼小、高度自動化、響應速度快等特點。
    特别适合于要求實時和多任務的應用體系。嵌入式實時系統是目前蓬勃發展的行業之一。 
    但是,實時嵌入式系統的特點使得其軟體受時間和空間的嚴格限制,
    加上運作環境複雜,使得嵌入式系統軟體的開發變得異常困難。 
    為了設計一個滿足功能、性能和死線要求的系統,
    為了開發出安全可靠的高性能嵌入式系統,開發語言的選擇十分重要。

2.嵌入式實時程式設計中語言的選擇
    随着嵌入式系統應用範圍的不斷擴大和 
    嵌入式實時作業系統RTOS(Real Time Operating System)的廣泛使用,
    進階語言程式設計已是嵌入式系統設計的必然趨勢。
    因為彙編語言和 具體的微處理器 的硬體結構密切相關,移植性較差,既不宜在複雜系統中使用,又不便于實作軟體重用;
    而進階語言具有良好的通用性和豐富的軟體支援,便于推廣、易于維護,是以進階語言程式設計具有許多優勢。
    目前,在嵌入式系統開發過程中使用的語言種類很多,但僅有少數幾種語言得到了比較廣泛的應用。
    其中C和C++是應用最廣泛的。C++ 在支援現代軟體工程、 OOP(Object Oriented Programming,面向對象的程式設計)、
    結構化等方面對C進行了卓有成效的改進,但在程式代碼容量、執行速度、 程式複雜程度等方面比C語言程式性能差一些。
    由于C語言既有低級語言的直接控制硬體的能力,又有進階語言的靈活性,是目前在嵌入式系統中應用最廣泛的程式設計語言。
    随着網絡技術和嵌入式技術的不斷發展,Java的應用也得到廣泛應用。

3.C/C++代碼在實時程式設計中的優化
    雖然使軟體正确是一個工程合乎邏輯的最後一個步驟,但是在嵌入式的系統開發中,情況并不總是這樣的。
    出于對低價産品的需求, 硬體的設計者需要提供剛好足夠的存儲器和完成工作的處理能力。
    是以在嵌入式軟體設計的最後一個階段則變成了對代碼的優化。

    現代的C和C++編譯器都提供了一定程度上的代碼優化。
    然而,大部分由編譯器執行的優化僅 涉及執行速度和代碼大小 的一個平衡。
    你的程式能夠變得更快或者更小,但是不可能又變快又變小。
    經過本人在嵌入式系統設計和實作過程中實踐,下面介紹幾種簡單且行之有效的C/C++代碼的優化方法。

1) Inline函數
    在C++中,關鍵字Inline 可以被加入到任何函數的聲明中。
    這個關鍵字 請求編譯器用 函數内部的代碼替換所有對于指出的函數的調用。 
    這樣做在兩個方面快于函數調用。這樣做在兩個方面快于函數調用:
    第一,省去了調用指令需要的執行時間;
    第二,省去了傳遞變元 和 傳遞過程需要的時間。
    但是使用這種方法在優化程式速度的同時,程式長度變大了,是以需要更多的ROM。
    使用這種優化在Inline函數頻繁調用并且隻包含幾行代碼的時候是最有效的。

2)用指針 代替 數組
    在許多種情況下,可以用指針運算 代替數組索引,這樣做常常能産生又快又短的代碼。
    與數組索引相比,指針一般能使代碼速度更快,占用空間更少。
    使用多元數組時差異更明顯。
    下面的代碼作用是相同的,但是效率不一樣。 

數組索引 指針運算 
           
for(;;)
    { 
    p=array 
    A=array[t++];
    for(;;)
    { 
        a=*(p++); 
        ...... ...... 
    } 
}
           
指針方法的優點是,array的位址每次裝入位址p後,在每次循環中隻需對p增量操作。
在數組索引方法中,每次循環中都必須進行基于t值求數組下标的複雜運算。

3)不定義 不使用的傳回值
    function函數定義 并不知道函數 傳回值是否被使用,
    假如傳回值從來不會被用到,
    應該使用void來明确聲明函數不傳回任何值。

4)手動編寫彙編
    在嵌入式軟體開發中,一些軟體子產品最好用彙編語言來寫,這可以使程式更加有效。
    雖然C/C++編譯器對代碼進行了優化,但是适當的 使用 内聯彙編指令 可以有效的提高整個系統運作的效率。

5)使用寄存器變量
    在聲明 局部變量 的時候可以使用 register關鍵字。
    這就使得編譯器把變量放入一個多用途的寄存器中,而不是在堆棧中,
    合理使用這種方法可以提高執行速度。
    函數調用越是頻繁,越是可能提高代碼的速度。

6)使用增量和減量操作符
    在使用到加一和減一操作時盡量使用增量和減量操作符,因為增量符語句比指派語句更快,
    原因在于對大多數CPU來說,對記憶體字的增、 減量操作不必明顯地使用取記憶體和寫記憶體的指令,
    比如下面這條語句: 
x=x+1; 
模仿大多數微機彙編語言為例,産生的代碼類似于:
           
move A,x ;把x從記憶體取出存入累加器A 
add A,1 ;累加器A加1 
store x ;把新值存回x
           
如果使用增量操作符,生成的代碼如下: 
incr x ; x加1 
顯然,不用取指令和存指令,增、減量操作執行的速度加快,同時長度也縮短了。

7)減少函數調用參數  
    使用全局變量比函數傳遞參數更加有效率。
    這樣做去除了函數調用參數入棧和函數完成後參數出棧所需要的時間。
    然而決定使用全局變量會影響程式的子產品化和重入,故要慎重使用。

8)Switch語句中 根據 發生頻率 來 進行case排序

switch語句是一個普通的程式設計技術,編譯器會産生if-else-if的嵌套代碼,
并按照順序進行比較,發現比對時,就跳轉到滿足條件的語句執行。
使用時需要注意。每一個由機器語言實作的測試和跳轉僅僅是為了決定下一步要做什麼,就把寶貴的處理器時間耗盡。
為了提高速度,沒法把具體的情況按照它們發生的相對頻率排序。
換句話說,把最可能發生的情況放在第一位,最不可能的情況放在最後。

9)将大的switch語句轉為嵌套switch語句
    當switch語句中的case标号很多時,為了減少比較的次數,明智的做法是把大switch語句轉為嵌套switch語句。
    把發生頻率高的case 标号放在一個switch語句中,并且是嵌套switch語句的最外層,
    發生相對頻率相對低的case标号放在另一個switch語句中。
    比如,下面的程式段把相對發生頻率低的情況放在預設的case标号内。 
           
pMsg=ReceiveMessage();

switch (pMsg->type) 
{ 
    case FREQUENT_MSG1: 
    handleFrequentMsg(); 
    break; 
    
    case FREQUENT_MSG2: 
    handleFrequentMsg2(); 
    break; 
    
    ...
    
    case FREQUENT_MSGn: 
    handleFrequentMsgn(); 
    break; 
    
    default: //嵌套部分用來處理不經常發生的消息 ====
    switch (pMsg->type) 
    { 
        case INFREQUENT_MSG1: 
        handleInfrequentMsg1(); 
        break; 
        
        case INFREQUENT_MSG2: 
        handleInfrequentMsg2(); 
        break; 
        
        ......
        
        case INFREQUENT_MSGm: 
        handleInfrequentMsgm(); 
        break; 
    } 
} 

           
如果switch中每一種情況下都有很多的工作要做,
那麼把整個switch語句用一個指向函數指針的表 來替換會更加有效,
比如下面的switch語句,有三種情況: 
           
enum MsgType{Msg1, Msg2, Msg3} 
switch (ReceiveMessage() 
{ 
case Msg1; 
...... 
case Msg2; 
..... 
case Msg3; 
..... 
}
           

為了提高執行速度,用下面這段代碼來替換這個上面的switch語句。

/*準備工作*/ 
int handleMsg1(void); 
int handleMsg2(void); 
int handleMsg3(void); 
/*建立一個函數指針數組*/ 
int (*MsgFunction [])()={handleMsg1, handleMsg2, handleMsg3};//函數指針數組 
/*用下面這行更有效的代碼來替換switch語句*/

status=MsgFunction[ReceiveMessage()]();
           
10)避免使用C++的昂貴特性
    C++在支援現代軟體工程、OOP、結構化等方面對C進行了卓有成效的改進,
    但在程式代碼容量、執行速度、程式複雜程度等方面比C語言程式性能差一些。
    并不是所有的C++特性都是肮貴的。
    比如,類的定義是完全有益的。
    公有和私有成員資料及函數的清單與一個 struct 及函數原形的清單并沒有多大的差别。
    單純的加入類既不會影響代碼的大小,也不會影響程式的效率。

    但C++的多重繼承、虛拟基類、模闆、 異常處理及運作類型識别等特性對代碼的大小和效率有負面的影響,
    是以對于C++的一些特性要慎重使用,可做些實驗看看它們對應用程式的影響。

4 總結語
    在嵌入式實時程式設計時可以運用上面介紹的一種或多種技術來優化代碼。
    上面介紹的方法主要是為了提高代碼的效率。
    但是事實上,在使用這些技術提高代碼運作速度的同時會相應的産生一些負面的影響,
    比如增加代碼的大小、降低程式可讀性等。
    不過你可以讓C/C++編 譯器來進行減少代碼大小的優化,而手動利用以上技術來減少代碼的執行時間。
    在嵌入式程式設計中合理地使用這幾種技術有時會達到很好 的優化效果。
           

C/C++代碼優化具體方案

C/C++代碼優化具體方案

目錄

1、選擇合适的算法和資料結構 3 
2、使用盡量小的資料類型 3 
3、減少運算的強度 3 
(1)查表 3 
(2)求餘運算 4 
(3)平方運算 4 
(4)用移位實作乘除法運算 4 
(5)避免不必要的整數除法 5 
(6)使用增量和減量操作符 5 
(7)使用複合指派表達式 6 
(8)提取公共的子表達式 6 
4、結構體成員的布局 7 
(1)按資料類型的長度排序 7 
(2)把結構體填充成最長類型長度的整倍數 7 
(3)按資料類型的長度排序本地變量 7 
(4)把頻繁使用的指針型參數拷貝到本地變量 8 
5、循環優化 9 
(1)充分分解小的循環 9 
(2)提取公共部分 9 
(3)延時函數 10 
(4)while循環和do…while循環 10 
(5)循環展開 10 
(6)循環嵌套 11 
(7)Switch語句中根據發生頻率來進行case排序 12 
(8)将大的switch語句轉為嵌套switch語句 13 
(9)循環轉置 14 
(10)公用代碼塊 15 
(12)選擇好的無限循環 16 
6、提高CPU的并行性 16 
(1)使用并行代碼 16 
(2)避免沒有必要的讀寫依賴 17 
7、循環不變計算 17 
8、函數優化 18 
(1)Inline函數 18 
(2)不定義不使用的傳回值 20 
(3)減少函數調用參數 20 
(4)所有函數都應該有原型定義 20 
(5)盡可能使用常量(const) 21 
(6)把本地函數聲明為靜态的(static) 21 
(7)Virtual function的運作期負擔 21 
9、采用遞歸及聲明放置 22 
(1)請使用初始化而不是指派 22 
(2)把聲明放在合适的位置上 22 
(3)初始化清單 23 
10、變量 24 
(1)register變量 24 
(2)同時聲明多個變量優于單獨聲明變量 25 
(3)短變量名優于長變量名,應盡量使變量名短一點 25 
(4) 在循環開始前聲明變量 25 
(5) 把那些保持不變的對象聲明為const 25 
11、使用嵌套的if結構 25
           

1、選擇合适的算法和資料結構

選擇一種合适的資料結構很重要,如果在一堆随機存放的數中使用了大量的插入和删除指令,那使用連結清單要快得多。
數組與指針語句具有十分密切的關系,一般來說,指針比較靈活簡潔,而數組則比較直覺,容易了解。
對于大部分的編譯器,使用指針比使用數組生成的代碼更短,執行效率更高。 
在許多種情況下,可以用指針運算代替數組索引,這樣做常常能産生又快又短的代碼。
與數組索引相比,指針一般能使代碼速度更快,占用空間更少。
使用多元數組時差異更明顯。
           

2、使用盡量小的資料類型

能夠使用字元型(char)定義的變量,就不要使用整型(int)變量來定義;
能夠使用整型變量定義的變量就不要用長整型(long int),
能不使用浮點型(float)變量就不要使用浮點型變量。
當然,在定義變量後不要超過變量的作用範圍,如果超過變量的範圍指派,
C編譯器并不報錯,但程式運作結果卻錯了,而且這樣的錯誤很難發現。 

在ICCAVR中,可以在Options中設定使用printf參數,
盡量使用基本型參數(%c、%d、%x、%X、%u和%s格式說明符),
少用長整型參數(%ld、%lu、%lx和%lX格式說明符),
至于浮點型的參數(%f)則盡量不要使用,其他C編譯器也一樣。
在其他條件不變的情況下,使用%f參數,
會使生成的代碼的數量增加很多,執行速度降低。
           

3、減少運算的強度

(1)查表

一個聰明的遊戲大蝦,基本上不會在自己的主循環裡搞什麼運算工作,絕對是先計算好了,再到循環裡查表。看下面的例子:

舊代碼:

long factorial(int i) // 階乘                                                         
    {
        if (i == 0)
            return 1;
        else
            return i * factorial(i - 1);
    }
           

新代碼:

static long factorial_table[] =
        {1, 1, 2, 6, 24, 120, 720  // etc  };
    long factorial(int i)
    {
        return factorial_table[i];
    }
           
如果表很大,不好寫,就寫一個init函數,在循環外臨時生成表格。
           
(2)求餘運算
a=a%8;      // 求2n方的餘數, 2^3=8
// 可以改為: 
a=a&7;      // & (2^n - x)

// 說明:位操作隻需一個指令周期即可完成,而大部分的C編譯器的”%”運算均是調用子程式來完成,代碼長、執行速度慢。
通常,隻要求是求2n方的餘數,均可使用位操作的方法來代替。
           
(3)平方運算
a=pow(a, 2.0); 
//可以改為: 
a=a*a;
/*
說明:在有内置硬體乘法器的單片機中(如51系列),乘法運算比求平方運算快得多,
因為浮點數的求平方是通過調用子程式來實作的,在自帶硬體乘法器的AVR單片機中,
如ATMega163中,乘法運算隻需2個時鐘周期就可以完成。
既使是在沒有内置硬體乘法器的AVR單片機中,
乘法運算的子程式比平方運算的子程式代碼短,執行速度快。
*/

// 如果是求3次方,如: 
a=pow(a,3.0); 
// 更改為: 
a=a*a*a; 
//則效率的改善更明顯。
           
(4)用移位 實作 乘除法 運算
a=a*4; 
b=b/4; 
// 可以改為: 
a=a<<2; 
b=b>>2; 
/*
通常如果需要乘以或除以2n,都可以用移位的方法代替。
在ICCAVR中,如果乘以2n,都可以生成左移的代碼,
而乘以其他的整數或除以任何數,均調用乘除法子程式。
用移位的方法得到代碼比調用乘除法子程式生成的代碼效率高。
實際上,隻要是乘以或除以一個整數,均可以用移位的方法得到結果,如: 
*/

a=a*9 
//可以改為: 
a=(a<<3)+a // a*2^3 + a  = 9*a
// 采用運算量更小的表達式替換原來的表達式,下面是一個經典例子: 
// 舊代碼: 
x = w % 8; 
y = pow(x, 2.0); 
z = y * 33; 
for (i = 0;i < MAX;i++) 
{ 
h = 14 * i; 
printf(“%d”, h); 
} 
// 新代碼: 
x = w&7; // w%8 ---> w&7 位操作比求餘運算快 
y = x*x; // pow(x, 2.0) ---> x*x 乘法比平方運算快 
z = (y << 5) + y; // y*33 ---> y*2^5 +y 位移乘法比乘法快 
for (i = h = 0; i < MAX; i++) 
{ 
h += 14; // 14 * i ---> += 14 加法比乘法快 ======!!!!!======
printf(“%d”, h); 
}

           
(5)避免不必要的整數除法

整數除法是整數運算中最慢的,是以應該盡可能避免。

一種可能減少整數除法的地方 是 連除, 這裡除法可以由乘法代替。

這個替換的副作用是有可能在算乘積時會溢出,是以隻能在一定範圍的除法中使用。

// 舊代碼: 
int i, j, k, m; 
m = i / j / k; 
// 新代碼: 
int i, j, k, m; 
m = i / (j * k);
           
(6)使用增量和減量操作符
在使用到加一和減一操作時盡量使用增量和減量操作符,因為增量符語句比指派語句更快,
原因在于對大多數CPU來說,對記憶體字的增、減量操作不必明顯地使用取存儲器和寫存儲器的指令,
比如下面這條語句: 
x=x+1; 
模仿大多數微機彙編語言為例,産生的代碼類似于: 
move A,x ;把x從存儲器取出存入累加器A 
add A,1 ;累加器A加1 
store x ;把新值存回x 
如果使用增量操作符源代碼如下: 
++x; 
生成的代碼如下: 
incr x ;x加1 
顯然,不用取指令和存指令,增、減量操作執行的速度加快,同時長度也縮短了。 
還有,最好用前置,後置需要儲存一次。
           
(7)使用複合指派表達式
// 複合指派表達式(如a-=1及a+=1等)都能夠生成高品質的程式代碼。 
// 舊代碼: 
a=a+b; 
// 新代碼: 
a+=b;
           
(8)提取公共的子表達式
在某些情況下,C++編譯器不能從浮點表達式中提出公共的子表達式,因為這意味着相當于對表達式重新排序。
需要特别指出的是,編譯器在提取公共子表達式前不能按照代數的等價關系重新安排表達式。
這時,程式員要手動地提出公共的子表達式(在VC.NET裡有一項”全局優化”選項可以完成此工作,但效果就不得而知了)。 
舊代碼: 
float a, b, c, d, e, f; 
...
e = b * c / d;  // 含 b/d
f = b / d * a;  // 也含 b/d
新代碼: 
float a, b, c, d, e, f; 
...
const float t(b / d); 
e = c * t; 
f = a * t; 
舊代碼: 
float a, b, c, e, f; 
...
e = a / c; // 都除以c 也就是包含 1.0f / c
f = b / c; 
新代碼: 
float a, b, c, e, f; 
...
const float t(1.0f / c); 
e = a * t; 
f = b * t;
           

4、結構體成員的布局

很多編譯器有”使結構體字,雙字或四字對齊”的選項。
但是,還是需要改善結構體成員的對齊,有些編譯器可能配置設定給結構體成員空間的順序與他們聲明的不同。
但是,有些編譯器并不提供這些功能,或者效果不好。
是以,要在付出最少代價的情況下實作最好的結構體和結構體成員對齊,建議采取下列方法:
           
(1)按資料類型的長度排序
把結構體的成員按照它們的類型長度排序,聲明成員時把長的類型放在短的前面。
編譯器要求把長型資料類型存放在偶數位址邊界。
在申明一個複雜的資料類型 (既有多位元組資料又有單位元組資料) 時,
應該首先存放多位元組資料,然後再存放單位元組資料,這樣可以避免存儲器的空洞。
編譯器自動地把結構的執行個體對齊在記憶體的偶數邊界。
           
(2)把結構體填充成最長類型長度的整倍數
把結構體填充成最長類型長度的整倍數。
照這樣,如果結構體的第一個成員對齊了,所有整個結構體自然也就對齊了。
下面的例子示範了如何對結構體成員進行重新排序:
           

舊代碼: //普通順序

struct
{
char a[5];
long k;
double x;
baz;
}
           

新代碼: //新的順序并手動填充了幾個位元組

struct
{
double x;  // 長的
long k;
char a[5];
char pad[7];// 并手動填充了幾個位元組 5+7=12=3*4  4位元組 對齊
baz;
}
           

這個規則同樣适用于類的成員的布局!!!。

(3)按資料類型的長度排序本地變量
當編譯器配置設定給本地變量空間時,它們的順序和它們在源代碼中聲明的順序一樣,
和上一條規則一樣,應該 把 長的變量 放在  短的變量前面。
如果第一個變量對齊了,其他變量就會連續的存放,而且不用填充位元組自然就會對齊。
有些編譯器在配置設定變量時不會自動改變變量順序,
有些編譯器不能産生4位元組對齊的棧,是以4位元組可能不對齊。
下面這個例子示範了本地變量聲明的重新排序: 
           

舊代碼,普通順序

short ga, gu, gi; 
long foo, bar; 
double x, y, z[3]; 
char a, b; 
float baz; 
           

新代碼,改進的順序

double z[3];   // 長的(老大)放在前面
double x, y; 
long foo, bar; 
float baz; 
short ga, gu, gi;
char a, b; 
           
(4)把頻繁使用的指針型參數 拷貝到 本地變量
避免在函數中 頻繁使用 指針型參數 指向的值。
因為編譯器不知道指針之間是否存在沖突,是以指針型參數往往不能被編譯器優化。
這樣資料不能被存放在寄存器中,而且明顯地占用了存儲器帶寬。
注意,很多編譯器有”假設不沖突”優化開關(在VC裡必須手動添加編譯器指令行/Oa或/Ow),
這允許編譯器假設兩個不同的指針總是有不同的内容,這樣就不用把指針型參數儲存到本地變量。
否則,請在函數一開始把指針指向的資料儲存到本地變量。如果需要的話,在函數結束前拷貝回去。
           

舊代碼:

// 假設 q != r 
void isqrt(unsigned long a, unsigned long* q, unsigned long* r) 
{ 
  *q = a; // 
  if (a > 0) 
  { 
    while (*q > (*r = a / *q)) 
    { 
      *q = (*q + *r) >> 1; 
    } 
  } 
  r = a - *q * q; 
} 
           

新代碼:

// 假設 q != r 
void isqrt(unsigned long a, unsigned long* q, unsigned long* r) 
{ 
  unsigned long qq, rr; // 中間變量,存儲對應 兩個指針指向的 傳回值
  qq = a; 
  if (a > 0) 
  { 
    while (qq > (rr = a / qq)) 
    { 
      qq = (qq + rr) >> 1; // 除以2
    } 
  } 
  rr = a - qq * qq; 
  *q = qq; // 最後把 計算結果(中間變量) 指派給 兩個指針指向的傳回值
  *r = rr; 
}
           

5、循環優化

(1)充分分解小的循環
要充分利用CPU的指令緩存(一個指令周期能夠讀取多個資料),就要充分分解小的循環。
特别是當循環體本身很小的時候,分解循環可以提高性能。
注意:很多編譯器并不能自動分解循環。
           

舊代碼: // 3D轉化:把矢量 V 和 4x4 矩陣 M 相乘

for (i = 0; i < 4; i ++) // 行
{ 
  r[i] = 0; 
  for (j = 0; j < 4; j ++) // 列
  { 
    r[i] += M[j][i]*V[j]; 
  } 
} 
           

新代碼:

r[0] = M[0][0]*V[0] + M[1][0]*V[1] + M[2][0]*V[2] + M[3][0]*V[3]; 
r[1] = M[0][1]*V[0] + M[1][1]*V[1] + M[2][1]*V[2] + M[3][1]*V[3]; 
r[2] = M[0][2]*V[0] + M[1][2]*V[1] + M[2][2]*V[2] + M[3][2]*V[3]; 
r[3] = M[0][3]*V[0] + M[1][3]*V[1] + M[2][3]*V[2] + M[3][3]*v[3];
           
(2)提取公共部分
對于一些不需要循環變量參加運算的任務可以把它們放到循環外面,
這裡的任務包括表達式、函數的調用、指針運算、數組通路等,
應該将沒有必要執行多次的操作全部集合在一起,放到一個init的初始化程式中進行。
           
(3)延時函數

通常使用的延時函數均采用自加的形式:

``c`

void delay (void)

{

unsigned int i;

for (i=0;i<1000;i++) ;

}

将其改為自減延時函數: 
```c
void delay (void) 
{ 
unsigned int i; 
for (i=1000;i>0;i–) ; 
} 
           
兩個函數的延時效果相似,但幾乎所有的C編譯對後一種函數生成的代碼均比前一種代碼少1~3個位元組,
因為幾乎所有的MCU均有,為0轉移的指令采用後一種方式能夠生成這類指令。
在使用while循環時也一樣,使用自減指令控制循環會比使用自加指令控制循環生成的代碼更少1~3個字母。
但是在循環中有通過循環變量”i”讀寫數組的指令時,使用預減循環有可能使數組超界,要引起注意。
           
(4)while循環和do…while循環

用while循環時有以下兩種循環形式:

unsigned int i; 
i=0; 
while (i<1000) 
{ 
    i++; 
    //使用者程式 
} 
// 或: 
unsigned int i; 
i=1000; 
do 
{ 
    i–-; 
    //使用者程式 
} 
while (i>0); 
// 在這兩種循環中,使用do…while循環編譯後生成的代碼的長度短于while循環。
           
(5)循環展開
這是經典的速度優化,但許多編譯程式(如gcc -funroll-loops)能自動完成這個事,
是以現在你自己來優化這個顯得效果不明顯。
           

舊代碼:

for (i = 0; i < 100; i++) 
{ 
    do_stuff(i); 
} 
           

新代碼:

for (i = 0; i < 100; ) 
{ 
do_stuff(i); i++; 
do_stuff(i); i++; 
do_stuff(i); i++; 
do_stuff(i); i++; 
do_stuff(i); i++; 
do_stuff(i); i++; 
do_stuff(i); i++; 
do_stuff(i); i++; 
do_stuff(i); i++; 
do_stuff(i); i++; 
} 
           
可以看出,新代碼裡比較指令由100次降低為10次(i每次循環會增加10),循環時間節約了90%。
不過注意:對于中間變量或結果被更改的循環,
編譯程式往往拒絕展開,(怕擔責任呗),這時候就需要你自己來做展開工作了。

還有一點請注意,在有内部指令cache的CPU上(如MMX晶片),因為循環展開的代碼很大,往往cache溢出,
這時展開的代碼會頻繁地在CPU 的cache和存儲器之間調來調去,又因為cache速度很高,是以此時循環展開反而會變慢。
還有就是循環展開會影響矢量運算優化。
           
(6)循環嵌套

把相關循環放到一個循環裡,也會加快速度。

舊代碼:

for (i = 0; i < MAX; i++) // initialize 2d array to 0’s 
for (j = 0; j < MAX; j++) 
    a[i][j] = 0.0; 
for (i = 0; i < MAX; i++) // put 1’s along the diagonal 
    a[i][i] = 1.0; 
           

新代碼:

for (i = 0; i < MAX; i++) // initialize 2d array to 0’s 
{ 
for (j = 0; j < MAX; j++) 
    a[i][j] = 0.0; 
a[i][i] = 1.0; // put 1’s along the diagonal  對角線1
}
           
(7)Switch語句中根據發生頻率來進行case排序
Switch 可能轉化成多種不同算法的代碼。其中最常見的是跳轉表和比較鍊/樹。
當switch用比較鍊的方式轉化時,編譯器會産生if-else-if的嵌套代碼,
并按照順序進行比較,比對時就跳轉到滿足條件的語句執行。
是以可以對case的值依照發生的可能性進行排序,把最有可能的放在第一位,這樣可以提高性能。
此外,在case中推薦使用小的連續的整數,因為在這種情況下,所有的編譯器都可以把switch 轉化成跳轉表。 
           

舊代碼:

int days_in_month, short_months, normal_months, long_months; 
.....
switch (days_in_month) 
{ 
  case 28: 
  case 29: 
    short_months ++; // 短 的 月份
    break; 
  case 30: 
    normal_months ++; // 正常的月份
    break; 
  case 31: 
    long_months ++;   // 較長的月份
    break; 
  default: 
    cout << “month has fewer than 28 or more than 31 days” << endl; 
    break; 
} 
           

新代碼:

int days_in_month, short_months, normal_months, long_months; 
...
switch (days_in_month) 
{ 
  case 31: 
    long_months ++; // 31天的和30天的 出現的較為常見 出現頻率較高
    break; 
  case 30: 
    normal_months ++; 
    break; 
  case 28: 
  case 29: 
    short_months ++; // 28\29 天的 少見,出現頻率低
    break; 
  default: 
    cout << “month has fewer than 28 or more than 31 days” << endl; 
    break; 
}
           
(8)将大的switch語句轉為嵌套switch語句
當switch語句中的case标号很多時,為了減少比較的次數,明智的做法是把大switch語句轉為嵌套switch語句。
把發生頻率高的case 标号放在一個switch語句中,并且是嵌套switch語句的最外層,
發生相對頻率相對低的case标号放在另一個switch語句中。
比如,下面的程式段把相對發生頻率低的情況放在預設的case标号内。 
           
pMsg=ReceiveMessage(); 
switch (pMsg->type) 
{ 
case FREQUENT_MSG1: 
    handleFrequentMsg(); 
    break; 
case FREQUENT_MSG2: 
    handleFrequentMsg2(); 
    break; 
...
case FREQUENT_MSGn: 
    handleFrequentMsgn(); 
    break; 
default: //嵌套case部分用來處理不經常發生的消息 
switch (pMsg->type) 
{ 
case INFREQUENT_MSG1: 
    handleInfrequentMsg1(); 
    break; 
case INFREQUENT_MSG2: 
    handleInfrequentMsg2(); 
    break; 
......
case INFREQUENT_MSGm: 
    handleInfrequentMsgm(); 
    break; 
} 
} 
           

如果switch中每一種情況下都有很多的工作要做,那麼把整個switch語句用一個指向函數指針的表來替換會更加有效,比如下面的switch語句,有三種情況:

enum MsgType{Msg1, Msg2, Msg3} 
switch (ReceiveMessage() )
{ 
case Msg1; 
... 
case Msg2; 
...
case Msg3; 
...
} 
           

為了提高執行速度,用下面這段代碼來替換這個上面的switch語句。

//準備工作 
int handleMsg1(void); 
int handleMsg2(void); 
int handleMsg3(void); 
//建立一個函數指針數組 
int (*MsgFunction [])()={handleMsg1, handleMsg2, handleMsg3}; // 函數指針數組 傳回值為int類型,輸入類型無
//用下面這行更有效的代碼來替換switch語句 
status=MsgFunction[ReceiveMessage()]();
           

(9)循環轉置

有些機器對JNZ(為0轉移)有特别的指令處理,速度非常快,如果你的循環對方向不敏感,可以由大向小循環。

舊代碼:

for (i = 1; i <= MAX; i++) // 循環變量 小 ---> 大
{ 
...
} 
           

新代碼:

i = MAX+1; 
while (–i) // 循環變量 大 ----> 小
{ 
...
} 
           

不過千萬注意,如果指針操作使用了i值,這種方法可能引起指針越界的嚴重錯誤(i = MAX+1;)。

當然你可以通過對i做加減運算來糾正,但是這樣就起不到加速的作用,除非類似于以下情況:

舊代碼:

char a[MAX+5]; 
for (i = 1; i <= MAX; i++) 
{ 
*(a+i+4)=0; 
} 
// 新代碼:
i = MAX+1; 
while (–i) 
{ 
*(a+i+4)=0; // 防止i為父,反向越界
}
           
(10)公用代碼塊
一些公用處理子產品,為了滿足各種不同的調用需要,往往在内部采用了大量的if-then-else結構,
這樣很不好,判斷語句如果太複雜,會消耗大量的時間的,應該盡量減少公用代碼塊的使用。
(任何情況下,空間優化和時間優化都是對立的–東樓)。
當然,如果僅僅是一個(3==x)之類的簡單判斷,适當使用一下,也還是允許的。
記住,優化永遠是追求一種平衡,而不是走極端。
           

(11)提升循環的性能

要提升循環的性能,減少多餘的常量計算非常有用(比如,不随循環變化的計算)。
           

舊代碼(在for()中包含不變的if()):

for( i ... ) 
{ 
  if( CONSTANT0 )   // 循環内部,判斷會執行多次
  { 
    DoWork0( i ); // 假設這裡不改變CONSTANT0的值 
  } 
  else 
  { 
    DoWork1( i ); // 假設這裡不改變CONSTANT0的值 
  } 
} 
新代碼: 

if( CONSTANT0 ) // 判斷隻做一次
{ 
  for( i ... ) 
  { 
    DoWork0( i ); 
  } 
} 
else 
{ 
  for( i ... ) 
  { 
    DoWork1( i ); 
  } 
}
           

如果已經知道if()的值,這樣可以避免重複計算。

雖然舊代碼中的分支可以簡單地預測,但是由于新代碼在進入循環前分支已經确定,就可以減少對分支預測的依賴。

(12)選擇好的無限循環 for (;? 優于 while (1)

在程式設計中,我們常常需要用到無限循環,常用的兩種方法是while (1) 和 for (;?。

這兩種方法效果完全一樣,但那一種更好呢?然我們看看它們編譯後的代碼:

// 編譯前: 
while (1); 
// 編譯後: 
mov eax,1 
test eax,eax 
je foo+23h 
jmp foo+18h 
           
編譯前: 
for (;;); 
編譯後: 
jmp foo+23h 
顯然,for (;;)指令少,不占用寄存器,而且沒有判斷、跳轉,比while (1)好。
           

6、提高CPU的并行性

(1)使用并行代碼
盡可能把長的有依賴的代碼鍊分解成幾個可以在流水線執行單元中并行執行的沒有依賴的代碼鍊。
很多進階語言,包括C++,并不對産生的浮點表達式重新排序,因為那是一個相當複雜的過程。
需要注意的是,重排序的代碼和原來的代碼在代碼上一緻并不等價于計算結果一緻,因為浮點操作缺乏精确度。
在一些情況下,這些優化可能導緻意料之外的結果。
幸運的是,在大部分情況下,最後結果可能隻有最不重要的位(即最低位)是錯誤的。
           
// 舊代碼: 
double a[100], sum;int i; 
sum = 0.0f; 
for (i=0; i<100; i++) // 100次循環
    sum += a[i]; 
    
// 新代碼: 
double a[100], sum1, sum2, sum3, sum4, sum; 
int i; 
sum1 = sum2 = sum3 = sum4 = 0.0; // 100次循環被分解成 25*4次循環
for (i = 0; i < 100; i += 4)  // 25次循環
{ 
sum1 += a[i]; // 1,5,9,...
sum2 += a[i+1]; // 2,6,10,...
sum3 += a[i+2]; // 3,7,11,...
sum4 += a[i+3]; // 4,8,12,...
} 
sum = (sum4+sum3)+(sum1+sum2); // 最後對 4段的和再次求和
           
要注意的是:使用4 路分解是因為這樣使用了4段流水線浮點加法,
浮點加法的每一個段占用一個時鐘周期,保證了最大的資源使用率。
           
(2)避免沒有必要的讀寫依賴
當資料儲存到記憶體時存在讀寫依賴,即資料必須在正确寫入後才能再次讀取。
雖然AMD Athlon等CPU有加速讀寫依賴延遲的硬體,允許在要儲存的資料被寫入記憶體前讀取出來,
但是,如果避免了讀寫依賴并把資料儲存在内部寄存器中,速度會更快。
在一段很長的又互相依賴的代碼鍊中,避免讀寫依賴顯得尤其重要。
如果讀寫依賴發生在操作數組時,許多編譯器不能自動優化代碼以避免讀寫依賴。
是以推薦程式員手動去消除讀寫依賴,舉例來說,引進一個可以儲存在寄存器中的臨時變量。
這樣可以有很大的性能提升。下面一段代碼是一個例子:
           
// 不好的代碼:
float x[VECLEN],y[VECLEN], z[VECLEN];
...
for (unsigned int k = 1; k < VECLEN; k++)
{
  x[k] = x[k-1] + y[k];
}

for (k = 1; k < VECLEN; k++)
{
  x[k] = z[k] * (y[k] - x[k-1]);
}

// 推薦的代碼:
float x[VECLEN], y[VECLEN],z[VECLEN];
...
float t(x[0]);
for (unsigned int k = 1; k < VECLEN; k++)
{
  t = t + y[k]; // 增加一個中間變量
  x[k] = t;
}

t = x[0];
for (k = 1; k <VECLEN; k++)
{
  t = z[k] * (y[k] - t);// 增加一個中間變量
  x[k] = t;
} 

           

7、循環不變計算

對于一些不需要循環變量參加運算的計算任務可以把它們放到循環外面,
現在許多編譯器還是能自己幹這件事,不過對于中間使用了變量的算式它們就不敢動了,是以很多情況下你還得自己幹。
對于那些在循環中調用的函數,凡是沒必要執行多次的操作通通提出來,放到一個init函數裡,循環前調用。
另外盡量減少喂食次數,沒必要的話盡量不給它傳參,需要循環變量的話讓它自己建立一個靜态循環變量自己累加,速度會快一點。

還有就是結構體通路,東樓的經驗,凡是在循環裡對一個結構體的兩個以上的元素執行了通路,
就有必要建立中間變量了(結構這樣,那C++的對象呢? 想想看),看下面的例子:
           
// 舊代碼:
    total =
        a->b->c[4]->aardvark +
        a->b->c[4]->baboon +
        a->b->c[4]->cheetah +
        a->b->c[4]->dog;

// 新代碼:
    struct animals * temp = a->b->c[4];// 建立結構體指針變量
    total =
        temp->aardvark +
        temp->baboon +
        temp->cheetah +
        temp->dog;

// 一些老的C語言編譯器不做聚合優化,而符合ANSI規範的新的編譯器可以自動完成這個優化,看例子:

    float a, b, c, d, f, g;
    ...
    a = b / c * d;
    f = b * g / c;

// 這種寫法當然要得,但是沒有優化

    float a, b,c,d,f,g;
    ...
    a = b / c * d;
    f = b / c * g;

// 如果這麼寫的話,一個符合ANSI規範的新的編譯器可以隻計算b/c一次,
// 然後将結果代入第二個式子,節約了一次除法運算。

           

8、函數優化

(1)Inline函數
在C++中,關鍵字Inline可以被加入到任何函數的聲明中。
這個關鍵字請求編譯器用函數内部的代碼替換所有對于指出的函數的調用。
這樣做在兩個方面快于函數調用:
    第一,省去了調用指令需要的執行時間;
    第二,省去了傳遞變元和傳遞過程需要的時間。
         但是使用這種方法在優化程式速度的同時,程式長度變大了,是以需要更多的ROM。
    使用這種優化在Inline函數頻繁調用并且隻包含幾行代碼的時候是最有效的。
           
(2)不定義不使用的傳回值
函數定義并不知道函數傳回值是否被使用,假如傳回值從來不會被用到,
應該使用void來明确聲明函數不傳回任何值。
           
(3)減少函數調用參數
使用全局變量 比 函數傳遞參數 更加有效率。
這樣做去除了函數調用參數入棧和函數完成後參數出棧所需要的時間。
然而決定 使用全局變量 會影響程式的子產品化和重入,故要慎重使用。
           
(4)所有函數都應該有原型定義
一般來說,所有函數都應該有原型定義。
原型定義可以傳達給編譯器更多的可能用于優化的資訊。
           
(5)盡可能使用常量(const)
盡可能使用常量(const)。
C++标準規定,如果一個const聲明的對象的位址不被擷取,允許編譯器不對它配置設定儲存空間。
這樣可以使代碼更有效率,而且可以生成更好的代碼。
           
(6)把本地函數聲明為靜态的(static)
如果一個函數隻在實作它的檔案中被使用,把它聲明為靜态的(static)以強制使用内部連接配接。
否則,預設的情況下會把函數定義為外部連接配接。這樣可能會影響某些編譯器的優化——比如,自動内聯。
           

9、采用遞歸 及 聲明放置

與LISP之類的語言不同,C語言一開始就病态地喜歡用重複代碼循環,
許多C程式員都是除非算法要求,堅決不用遞歸。
事實上,C編譯器們對優化遞歸調用一點都不反感,相反,它們還很喜歡幹這件事。
隻有在遞歸函數需要傳遞大量參數,可能造成瓶頸的時候,
才應該使用循環代碼,其他時候,還是用遞歸好些。

程式中變量和對象的聲明放在什麼位置将會對性能産生顯著影響。
同樣,對postfix和prefix運算符的選擇也會影響性能。
這一部分我們集中讨論四個問題:初始化v.s 指派,
在程式确實要使用的地方放置聲明,構造函數的初始化清單,
prefix v.s postfix運算符。
           
(1)請使用初始化而不是指派
在C語言中隻允許在一個函數體的開頭進行變量的聲明,然而在C++中聲明可以出現在程式的任何位置。
這樣做的目的是希望把對象的聲明拖延到确實要使用它的時候再進行。
這樣做可以有兩個好處:
  1. 確定了對象在它被使用前不會被程式的其他部分惡意修改。
     如果對象在開頭就被聲明然而卻在20行以後才被使用的話,就不能做這樣的保證。
  2. 使我們有機會通過用初始化取代指派來達到性能的提升,從前聲明隻能放在開頭,
     然而往往開始的時候我們還沒有獲得我們想要的值,是以初始化所帶來的好處就無法被應用。
     但是現在我們可以在我們獲得了想要的值的時候直接進行初始化,進而省去了一步。
     注意,或許對于基本類型來說,初始化和指派之間可能不會有什麼差異,
     但是對于使用者定義的類型來說,二者就會帶來顯著的不同,
     因為指派會多進行一次函數調用—-operator =。
     是以當我們在指派和初始化之間進行選擇的話,初始化應該是我們的首選。
           
(2)把聲明放在合适的位置上
在一些場合,通過移動聲明到合适的位置所帶來的性能提升應該引起我們足夠的重視。例如:
           
bool is_C_Needed(); 
  void use()
  {
      C c1;
      if (is_C_Needed() == false)
      {
          return; //c1 was not needed
      } 
      //use c1 here
      return; 
  }
  
           
上面這段代碼中對象c1即使在有可能不使用它的情況下也會被建立,
這樣我們就會為它付出不必要的花費,有可能你會說一個對象c1能浪費多少時間,
但是如果是這種情況呢:C c1[1000];我想就不是說浪費就浪費了。
但是我們可以通過移動聲明c1的位置來改變這種情況:
           
void use()
  {
      if (is_C_Needed() == false)
      {
          return; //c1 was not needed
      } 
      C c1; //moved from the block's beginning
      //use c1 here
      return; 
  }
           
怎麼樣,程式的性能是不是已經得到很大的改善了呢?
是以請仔細分析你的代碼,把聲明放在合适的位置上,它所帶來的好處是你難以想象的。     
           
(3)初始化清單
我們都知道,初始化清單一般是用來初始化const或者reference資料成員。
但是由于他自身的性質,我們可以通過使用初始化清單來實作性能的提升。
我們先來看一段程式:
           
class Person
  {
  private:
      C c_1;
      C c_2;
  public:
      Person(const C& c1, const C& c2 ): c_1(c1), c_2(c2) 
        // :号後面為 初始化清單,傳入參數指派給 内部私有參數 隻進行一次初始化
        {}
  };
  
// 當然構造函數我們也可以這樣寫:
Person::Person(const C& c1, const C& c2)
  {
      c_1 = c1; // 一次初始化 + 指派
      c_2 = c2;
  }
           
究竟二者會帶來什麼樣的性能差異呢,要想搞清楚這個問題,我們首先要搞清楚二者是如何執行的,
先來看初始化清單:資料成員的聲明操作都是在構造函數執行之前就完成了,
在構造函數中往往完成的隻是指派操作,然而初始化清單直接是在資料成員聲明的時候就進行了初始化,
是以它隻執行了一次copy constructor。
再來看在構造函數中指派的情況:首先,在構造函數執行前會通過default constructor建立資料成員,
然後在構造函數中通過operator =進行指派。是以它就比初始化清單多進行了一次函數調用。
性能差異就出來了。但是請注意,如果你的資料成員都是基本類型的話,
那麼為了程式的可讀性就不要使用初始化清單了,因為編譯器對兩者産生的彙編代碼是相同的。
           

10、變量

(1)register變量
在聲明局部變量的時候可以使用register關鍵字。
這就使得編譯器把變量放入一個多用途的寄存器中,而不是在堆棧中,
合理使用這種方法可以提高執行速度。
函數調用越是頻繁,越是可能提高代碼的速度。

在最内層循環避免使用全局變量和靜态變量,除非你能确定它在循環周期中不會動态變化,
大多數編譯器優化變量都隻有一個辦法,就是将他們置成寄存器變量,而對于動态變量,
它們幹脆放棄對整個表達式的優化。盡量避免把一個變量位址傳遞給另一個函數,雖然這個還很常用。
C語言的編譯器們總是先假定每一個函數的變量都是内部變量,
這是由它的機制決定的,在這種情況下,它們的優化完成得最好。
但是,一旦一個變量有可能被别的函數改變,這幫兄弟就再也不敢把變量放到寄存器裡了,嚴重影響速度。
看例子: 
           
a = b(); 
c(&d); 
           
因為d的位址被c函數使用,有可能被改變,編譯器不敢把它長時間的放在寄存器裡,
一旦運作到c(&d),編譯器就把它放回存儲器,如果在循環裡,會造成N次頻繁的在記憶體和寄存器之間讀寫d的動作,
衆所周知,CPU在系統總線上的讀寫速度慢得很。比如你的賽楊300,CPU主頻300,總線速度最多66M,為了一個總線讀,CPU可能要等4-5個周期.

register specifier被用來告訴編譯器一個對象将被會非常多的使用,可以把它放入寄存器中。
例如:
           
void f() 
  { 
   int *p = new int[3000000]; 
   register int *p2 = p; // 位址(指針) 存放在 寄存器變量中
   for (register int j = 0; j<3000000; j++) 
   { 
   *p2++ = 0; 
   } 
   //…use p 
   delete [] p; 
  }
           
循環計數是應用寄存器變量的最好的候選者。當它們沒有被存入一個寄存器中,
大部分的循環時間都被用在了從記憶體中取出變量和給變量賦新值上。
如果把它存入一個寄存器中的話,将會大大減少這種負擔。
需要注意的是,register specifier僅僅是對編譯器的一個建議。
就好比内聯函數一樣,編譯器可以拒絕把一個對象存儲到寄存器中。
另外,現代的編譯器都會通過把變量放入寄存器中來優化循環計數。
Register storage specifier并不僅僅局限在基本類型上,它能夠被應用于任何類型的對象。
如果對象太大而不能裝進寄存器的話,編譯器仍然能夠把它放入一個高速存儲器中,例如cache。
用register storage specifier聲明函數型參将會是建議編譯器把實參存入寄存器中而不是堆棧中。
例如:
           
(2)同時聲明多個變量優于單獨聲明變量

(3)短變量名優于長變量名,應盡量使變量名短一點

(4)在循環開始前聲明變量

(5)把那些保持不變的對象聲明為const 
通過把對象聲明為const,編譯器就可以利用這個聲明把這樣一個對象放入寄存器中。
           

11、使用嵌套的if結構

在if結構中如果要判斷的并列條件較多,最好将它們拆分成多個if結構,
然後嵌套在一起,這樣可以避免無謂的判斷。 

說明: 
     該方案主要是考慮到在嵌入式開發中對程式執行速度的要求特别高,是以該方案主要是為了優化程式的執行速度。 
注意:
     優化是有側重點的,優化是一門平衡的藝術,它往往要以犧牲程式的可讀性或者增加代碼長度為代價。 
(任何情況下,空間優化和時間優化都是對立的–東樓)。
           

總結

我做了很多關于程式執行速度方面優化的工(一個示例),我也看過其它人做的優化。
我發現有兩個最基本的優化技術總是被人所忽略。 
注意,這兩個技術并不是避免時機不成熟的優化。
并不是把冒泡排序變成快速排序(算法優化。

也不是語言或是編譯器的優化。

也不是把 i*4 寫成 i<<2 的優化。 

這兩個技術是:

    1. 使用 一個profiler。
    2. 檢視程式執行時的彙編碼。
    
使用這兩個技術的人将會成功地寫出運作快的代碼,不會使用這兩個技術的人則不行。    
           
使用一個 Profiler
我們知道,程式運作時的90%的時間是用在了10%的代碼上。
我發現這并不準确。一次又一次地,我發現,幾乎所有的程式會在1%的代碼上花了99%的運作時間。
但是,是哪個1%?一個好的 Profiler 可以告訴你這個答案。
就算我們需要使用100個小時在這1%的代碼上進行優化,也比使用100個小時在其它99%的代碼上優化産生的效益要高得多得多。 

問題是什麼?人們不用profiler?不是。
我工作過的一個地方使用了一個華麗而奢侈的Profiler,
但是自從購買這個Profiler後,它的包裝3年來還是那麼的暫新。

為什麼人們不用?我真的不知道。有一次,我和我的同僚去了一個負載過大的交易所,
我同僚堅持說他知道哪裡是瓶頸,畢竟,他是一個很有經驗的專家。
最終,我把我的Profiler在他的項目上運作了一下,我們發現那個瓶頸完全在一個意想不到的地方。 

就像是賽車一樣。團隊是赢在傳感器和日志上,這些東西提供了所有的一切。
你可以調整一下賽車手的褲子以讓其在比賽過程中更舒服,但是這不會讓你赢得比賽,也不會讓你更有競争力。
如果你不知道你的速度上不去是因為引擎、排氣裝置、空體動力學、輪胎氣壓,或是賽車手,那麼你将無法獲勝。

程式設計為什麼會不同呢?隻要沒有測量,你就永遠無法進步。
這個世界上有太多可以使用的Profiler了。
随便找一個你就可以看到你的函數的調用層次,調用的次數,以前每條代碼的時間分解表(甚至可以到彙編級)。
我看過太多的程式員回避使用Profiler,而是把時間花在那些無用的,錯誤的方向上的“優化”,而被其競争對手所羞辱。
(譯者陳皓注:使用Profiler時,重點需要關注:
   1)花時間多的函數以優化其算法,
   2)調用次數巨多的函數——如果一個函數每秒被調用300K次,你隻需要優化出0.001毫秒,那也是相當大的優化。
   這就是作者所謂的1%的代碼占用了99%的CPU時間)
           

C++ Profiler工具

GUN Gropf
Gprof是GNU profiler工具。可以顯示程式運作的“flatprofile”,
包括每個函數的調用次數,每個函數消耗的處理器時間。
也可以顯示“調用圖”,包括函數的調用關系,每個函數調用花費了多少時間。
還可以顯示“注釋的源代碼”,是程式源代碼的一個複本,标記有程式中每行代碼的執行次數。
           

C++ Profiler工具

檢視彙編代碼
幾年前,我有一個同僚,Mary Bailey,她在華盛頓大學教矯正代數(remedial algebra),
有一次,她在黑闆上寫下: x + 3 = 5 然後問他的學生“求解x”,然後學生們不知道答案。
于是她寫下:__ + 3 = 5 然後,再問學生“填空”,所有的學生都可以回答了。
未知數x 就像是一個有魔法的字母讓大家都在想“x意味着代數,而我沒有學過代數,是以我就不知道這個怎麼做”。 
彙程式設計式就是程式設計世界的代數。如果某人問我“inline函數是否被編譯器展開了?
”或是問我“如果我寫下i*4,編譯器會把其優化為左移位操作嗎?”。
這個時候,我都會建議他們看看編譯器的彙編碼。這樣的回答是不是很粗暴和無用?
通常,在我這樣回答了提問者後,提問都通常都會說,對不起,我不知道什麼是彙編!
甚至C++的專家都會這麼回答。 彙編語言是最簡單的程式設計語言了(就算是和C++相比也是這樣的),如:
           
ADD ESI,x
    # 就是(C風格的代碼)
    ESI += x;
    # 而:
    CALL foo
    # 則是:
    foo();
           
細節因為CPU的種類而不同,但這就是其如何工作的。
有時候,我們甚至都不需要細節,隻需要看看彙編碼的長啥樣,然後和源代碼比一比,你就可以知道彙編代碼很多很多了。 
那麼,這又如何幫助代碼優化?舉個例子,我幾年前認識一個程式員認為他應該去發現一個新的更快的算法。
他有一個benchmark來證明這個算法,并且其寫了一篇非常漂亮的文章關于他的這個算法。
但是,有人看了一下其原來算法以及新算法的彙編,發現了他的改進版本的算法允許其編譯器把兩個除法操作變成了一個。
這和算法真的沒有什麼關系。我們知道除法操作是一個很昂貴的操作,并且在其算法中,
這倆個除法操作還在一個内嵌循環中,是以,他的改進版的算法當然要快一些。
但,隻需要在原來的算法上做一點點小的改動——使用一個除法操作,那麼其原來的算法将會和新的一樣快。
而他的新發現什麼也不是。 下一個例子,一個D使用者張貼了一個 benchmark 來顯示 dmd (Digital Mars D 編譯器)在整型算法上的很糟糕,
而ldc (LLVM D 編譯器) 就好很多了。對于這樣的結果,其相當的有意見。
我迅速地看了一下彙編,發現兩個編譯器編譯出來相當的一緻,并沒有什麼明顯的東西要對2:1這麼大的不同而負責。
但是我們看到有一個對long型整數的除法,這個除法調用了運作庫。而這個庫成為消耗時間的殺手,其它所有的加減法都沒有速度上的影響
。出乎意料地,benchmark 和算法代碼生成一點關系也沒有,完全就是long型整數的除法的問題。
這暴露了在dmd的運作庫中的long型除法的實作很差。修正後就可以提高速度。
是以,這和編譯器沒有什麼關系,但是如果不看彙編,你将無法發現這一切。 
檢視彙編代碼經常會給你一些意想不到的東西讓你知道為什麼程式的性能是那樣。
一些意想不到的函數調用,預料不到的自傲,以及不應該存在的東西,等等其實所有的一切。
但也不需要成為一個彙編代碼的黑客才能幹的事。
           
結論
如果你覺得需要程式有更好的執行速度,
那麼,最基本的方法就是
1. 使用一個 profiler 和 願意去
2. 檢視 一下其彙編代碼 
以找到程式的瓶頸。
隻有找到了程式的瓶頸,此時才是真正在思考如何去改進的時候,比如思考一個更好的算法,使用更快的語言優化,等等。 
正常的做法是制勝法寶是挑選一個最佳的算法而不是進行微優化。

雖然這種做法是無可異議的,但是有兩件事情是學校沒有教給你而需要你重點注意的。

第一個也是最重要的,如果你優化的算法沒有參與到你程式性能中的算法,
    那麼你優化他隻是在浪費時間和精力,并且還轉移了你的注意力讓你錯過了應該要去優化的部分。
第二點,算法的性能總和處理的資料密切相關的,就算是冒泡排序有那麼多的笑柄,但是如果其處理的資料基本是排好序的,
    隻有其中幾個資料是未排序的,那麼冒泡排序也是所有排序算法裡性能最好的。
    
    是以,擔心沒有使用好的算法而不去測量,隻會浪費時間,無論是你的還是計算機的。 
    就好像賽車零件的訂購速底是不會讓你更靠進冠軍(就算是你正确安裝零件也不會),
    沒有Profiler,你不會知道問題在哪裡,
    不去看彙編,你可能知道問題所在,但你往往不知道為什麼。 
           
cpp程式優化 嵌入式C/C++代碼優化 C/C++代碼優化具體方案cpp程式優化

繼續閱讀