大概所有學習C語言的初學者,都被前輩說過,C語言是世界上接近最速的編程語言,當然這并不是吹牛,也并不是貶低其他語言,誠然非C語言能寫出高速度的代碼,但是C語言更容易寫出高速的程序(高速不代表高效),然而再好的工具,在外行人手中也只能是黯淡沒落。
對于現(xiàn)代編譯器,現(xiàn)代CPU而言,我們要盡量迎合CPU的設計(比如架構(gòu)和處理指令的方式等),雖然編譯器是為程序員服務,并且在盡它最大的能力來優(yōu)化程序員寫出的代碼,但是畢竟它還沒有脫離電子的范疇,如果我們的代碼不能讓編譯器理解,編譯器無法幫我們優(yōu)化代碼,那么我們就無法寫出一個高速的程序。
對于此,我們可以暫且忽略CPU的設計,因為我們在層面上只能考慮如何迎合編譯器的優(yōu)化規(guī)則,而CPU則是語言以及編譯器的事情了。
提高程序的速度,就C語言而言可以有這幾種方法:
注:隨著編譯器的版本更新,即使不開啟優(yōu)化選項,自帶的編譯器優(yōu)化依舊能夠為我們編寫的代碼提供一部分優(yōu)化,這便是不使用老版本編譯器的原因,雖然作為一個程序員不應該太依賴于編譯器,但是我認為,時代在進步,信息量正在無限的膨脹,但是人類的大腦以及精力在一個大時代內(nèi)是有限的,換句話說對于普通人而言我們的記憶是有限的,我們不應該把精力放在前人已經(jīng)做完的事情上,而是要站在巨人的肩膀上向更遠處眺望,如此我們應該充分利用工具來幫助我們實現(xiàn)一些既有的功能,而程序員應該更 專注于發(fā)現(xiàn)新的思路,以及想法,在圖靈測試尚未有人打破之前,程序員依賴編譯器并不是一件錯誤的事情。
對于當下的編譯器,以GCC(GCC不僅僅是一個編譯器,但這里將它當成編譯器的代名詞)為例,-O2是一個為大眾所接受的優(yōu)化等級,對于其他編譯器,一般程序員可以選擇使用由Google和Apple聯(lián)合開發(fā)的編譯器clang也是一個很好的選擇, 在-O2的優(yōu)化等級下,GCC一般情況下能夠自動執(zhí)行循環(huán)展開優(yōu)化,
.
/*struct.h*/
#include <stdio.h>
typedef struct me{
int value;
struct me* next;
}data_t;
typedef struct{
int index;
data_t* storage;
}block;
為了測試方便我們首先定義了兩個結(jié)構(gòu)體,分別是:
block代表一個塊,每個塊都有一個序號(int),一個數(shù)據(jù)域data_t
data_t代表一個數(shù)據(jù)域,原型是一個鏈表,每個data_t對象中包含一個數(shù)據(jù)和一個指針。
/*main.c*/
#include "struct.h"
#define ARR_SIZE 10
static inline int get_len(const data_t* data)
{
int len = 0;
if(!data)
fprintf(stderr,"The data in %p is NULL\n",data);
else
while(!data->next)
{
++len;
data = data->next;
}
return len;
}
static inline void mix_cal(const block* process, int result[])
{
for(int i = 0;i < get_len(process->storage);++i)
{
*result += (process->storage)[i];
}
}
此時我們得到了兩個測試函數(shù),get_len和mix_cal分別用來得到data_t長度,以及計算數(shù)據(jù)域的總和。
/*main.c*/
int main(void)
{
block* block_in_all[ARR_SIZE] = { NULL };
int result_in_all[ARR_SIZE] = { 0 };
/*
* 假設生成了許多的`block`類型對象
* 將許多的`block`放置在一個數(shù)組中,每個元素類型為`block*`
* 每個block對象中都包含非空的data_t類型的數(shù)據(jù)域
*/
for(int i = 0;i < ARR_SIZE;++i)
{
mix_cal(block_in_all[i], result_in_all+i);
}
for(int i = 0;i < ARR_SIZE;++i)
{
printf("The %dth block have the total %d data\n",
block_in_all[i]->index, result_in_all[i]);
}
return 0;
}
耐心讀完上述的代碼,它是用來求和的,求一個域中的所有元素的和。仔細分析一下,很容易就能看見一些缺點,最大的莫過于在mix_cal函數(shù)中對于get_len函數(shù)的調(diào)用,在此處看來十分明顯,但是我們在編寫程序的時候是否能夠注意到這個問題呢?
對于一些不必要的函數(shù)調(diào)用我們要做的便是將他們提取出來,使用臨時變量是一個很好的辦法,因為在編譯器的幫助下臨時變量在允許的情況下能夠充分的利用CPU的寄存器。之所以是允許的情況下,是因為寄存器的數(shù)量并不多,而編譯器在寄存器的使用上需要考慮許多的復雜因素,故并不是每次使用臨時變量都能加入寄存器。但這并不妨礙我們提升程序的性能。
在此處,我們應該將for循環(huán)中的判斷語句里的get_len函數(shù)提取出來,在外部使用一個臨時變量接收結(jié)果,而不是在循環(huán)中一直調(diào)用該函數(shù)。
int len = get_len(process->storage);
.
依舊是上方的代碼,我們來講述一下,循環(huán)展開。
對于mix_cal函數(shù),我們或者說編譯器可以如何提升它的速度呢?我們說過一點的小改變都可能對一個程序的最終代碼產(chǎn)生極大的影響,對此最常用的便是嘗試,前人之路早已鋪好,不需要重復造輪子了。
循環(huán)展開:
int reality = len - 1, i;
for(i = 0;i < reality;i+=2)
{
*result = *result + (process->storage)[i]
+ (process->storage)[i+1];
}
for(;i < len;++i)
{
*result += (process->storage)[i];
}
這就是循環(huán)展開中的2次循環(huán)展開,同樣還有n次循環(huán)展開。
同樣,在剛才提到過寄存器的使用以及減少不必要的開銷,在此程序中對于(process->storage)[i]這樣的存儲器位置解引用太過浪費,我們總是將其優(yōu)化成本低臨時變量的使用
data* local_data = process->storage;
這將為程序帶來十分可觀的節(jié)約,雖然這些工作在編譯器的優(yōu)化中都能包括,但是一旦我們的代碼難以被編譯器所理解(雖然編譯器的升級最大的目的就是提升優(yōu)化效果),那么我們很可能得到一個性能不夠可觀的程序。所以當我們并不是特別緊湊的時候,可以將這些工作當成我們的本分來做,而不是交給編譯器來做。
以及對于外部存儲位置 result 我們在此處也是存在著浪費,同樣我們應該使用一個臨時變量來存儲總和,而不是每次得到結(jié)果便對它進行解引用操作。
int local_result = 0;
/*...*/
local_result = local_result + local_data[i] + local_data[i+1];
/*...*/
*result = local_result;
在上方我們可以看見循環(huán)展開被稱作2次循環(huán)展開,那么自然可以推斷有n次循環(huán)展開,自然是有的,對于一個n次循環(huán)展開的式子我們有一個簡便的上界確定公式即:
reality = len - n + 1;
至于展開幾次最好,依然是視環(huán)境而定。 故最終的版本應該為:
static inline void mix_cal(const block* process, int result[])
{
int local_result = 0;
int len = get_len(process->storage);
int reality = len - 1, i;
data* local_data = process->storage;
for(i = 0;i < reality;i+=2)
local_result += local_data[i] + local_data[i+1];
for(;i < len;++i)
local_result += local_data[i];
*result = local_result;
}
解釋:循環(huán)展開將元素相加分為兩個部分,第一部分每次加兩個元素,由于如此做會剩余元素沒有加,故在第二部分將剩下的元素都加起來。
. 還有一種叫做重新組合的技巧,即為讓一個表達式中的運算數(shù)自由組合,組合出最快速的一種,但是這種方法未曾試驗過。故不提及。
. 對于條件分支預測錯誤造成的時間損耗,稱之為懲罰,最通俗的說法,就是當你編寫的代碼中含有條件分支的時候,處理器會選擇去預判某一個分支是此次正確的支路,這樣可以避免修改任何實際的寄存器和存儲器,一直到確定了實際結(jié)果,要是不對,那就慘了,這段時間做的事情都白費了。但是也不必過分的關心這種條件分支的預測,這也是我放在最后說的意義所在。
這里有兩種較為客觀的方法,一種被稱為命令式,一種被稱為功能式
命令式:
for(int i = 0;i < n;++i)
{
if(a[i] > b[i]){
int temp = a[i];
a[i] = b[i];
b[i] = temp;
}
}
功能式:
int min, max;
for(int i = 0;i < n;++i)
{
min = a[i] < b[i] ? a[i] : b[i];
max = a[i] < b[i] ? b[i] : a[i];
a[i] = min;
b[i] = max;
}
很清晰的一個例子,明顯看出來,前者對于不同情況所作的程序步數(shù)明顯不同,而后者無論什么情況都是相同的程序步。
兩個形式的好處前者對于可預測數(shù)據(jù)來說,是一個很好的模型,后者則是中庸之道,什么是可預測不可預測,比如一個數(shù)是負數(shù)還是正數(shù)這就是不可預測的,用前面那個代碼會有很大的懲罰。
. 多路并行的技巧也是一個很重要的思路,可能在很多人眼中看來,兩條語句依次寫出和合并的效果一定是一樣。但是多路并行有一個缺點就是對寄存器的數(shù)量有所要求,當寄存器不夠時(稱為溢出),性能不升反降。同樣是對于循環(huán)展開,此次使用四次循環(huán)展開加二路并行:
for(i = 0;i < reality;i+=4){
local_result_1 += local_data[i] + local_data[i+1];
local_result_2 += local_data[i+2] + local_data[i+3];
}//也可以分成四路并行,每一路存一個。這種做法充分利用了CPU流水線的性能
for(;i < len;++i)
local_result_1 += local_data[i];
*result = local_result_1 + local_result_2;
上文中寫到的函數(shù)大都帶有static inline關鍵字,這是何意?首先我們要確定一件事情,對于非工程的單文件而言,static函數(shù)并沒有什么意義(意義指的是對于可見性而言,并非說它一無是處),許多人對于static函數(shù)感到茫然的原因在于:我明明將一個函數(shù)聲明定義成static類型了,但是我還是可以在別的文件中訪問到啊!
其實這是因為你根本就沒有理解C語言工程這個意思,大部分人是這么測試的:
首先在一個文件夾里創(chuàng)建兩個文件 test_static.c和static.h:
/*static.h*/
#ifndef STATIC_H
#define STATIC_H
static void test(void);
static void test(void);
{
printf("Hello World!\n");
}
#endif
...
/*test_static.c*/
#include <stdio.h>
#include "static.h"
void test(void);
int main(void)
{
test(); //編譯通過,可以運行。
return 0;
}
然后編譯運行,發(fā)現(xiàn)可以通過?。?!標準怎么說在其他文件中不可見?而把static.h去掉#include之后發(fā)現(xiàn)報錯test undefined,瞬間初學者就凌亂了。
好吧,實際上是前輩們以及教材的錯,因為從始至終,所有外界現(xiàn)象都告訴我們C程序是獨立的一個一個文件組成的,但是并沒有告訴我們要先將他們弄成一個工程!此處如果是使用Visual Studio學習C語言的可能會對工程這個概念理解的稍微好一些,雖然不推薦使用 VS 學習C語言。
你想要實現(xiàn)static函數(shù)僅在本文件可見的效果,請你先補習一下工程這個概念,對于任何可見或者不可見的概念而言都是建立在一個工程內(nèi)而言,而不是像上方的代碼,使用#include來表示,你都#include了,那還有什么可見不可見的當然都可見了。所以一個static函數(shù)可見于不可見是基于一個個工程里的所有C語言源文件而言的。所以你將??匆娗拜厒冞@么回答你的提問:
/*static.h*/
#ifndef STATIC_H
#define STATIC_H
static void test(void);
static void test(void);
{
printf("Hello World!\n");
}
#endif
...
/*test_static.c*/
#include <stdio.h>
void test(void);
int main(void)
{
test(); //報錯,因為test是static函數(shù)。
return 0;
}
發(fā)現(xiàn)了嗎?在上方代碼中,少了一行#include "static.h"但是這個代碼依舊可行,因為這兩個文件是建立在同一個工程里的,而不是在一個文件夾中隨意新建兩個源文件這么簡單,你可以使用各個IDE的工程功能來進行測試。
回到正題,在這里稍微提一下static對函數(shù)的某些作用,它可以讓函數(shù)放在一個靜態(tài)的空間中,而不是棧里,這是的它的調(diào)用更加快速,經(jīng)常與inline關鍵字一起使用,為的就是讓函數(shù)更加快。但是有利有弊,可以自己權(quán)衡一下。