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