C語言在Linux系統中的重要性自然是無與倫比、不可替代,所以我寫Linux江湖系列不可能不提C語言。C語言是我的啟蒙語言,感謝C語言帶領我進入了程式世界。雖然現在不靠它吃飯,但是仍免不了經常和它打交道,特別是在Linux系統下。
Linux系統中普遍使用的是GNU-C,這裡有一份Gnu-C語言手冊.pdf。The GNU C Reference Manual的首頁在這裡:http://www.gnu.org/software/gnu-c-manual/。C語言的核心極其緊湊,該手冊總共只有91頁,去掉目錄、附錄和索引後就只有70頁。我一般一個多小時就可以將其從頭至尾複習一遍。我曾有過將其翻譯成中文的想法,後來還是放棄了。翻譯這種字斟句酌的事情還是讓別人來幹吧。我唯寫寫我自己的感悟。
感悟一:C語言標準幹不過GNU擴充
最近為了研究X Window的底層協議,開始嘗試使用XCB編程。當我開啟XCB的標頭檔的時候,我被大量的__restrict__關鍵字驚呆了,好在有GNU C語言手冊為我答疑解惑。__restrict__又是一個GNU擴充的關鍵字,後面我會詳細講解該關鍵字的用途。其實C語言的C99標準中已經引入了restrict關鍵字,沒有前後的底線,但是在大量的開原始碼中,使用最普遍的還是GNU的擴充,而不是C語言標準。
和restrict關鍵字有相同命運的還有inline、_Complex等,它們都是在C99標準中引入的關鍵字,但是其實在C99標準出來之前,GNU C中早就有了__inline__、__complex__等擴充關鍵字。還記得多年前我學習Linux 0.11版的原始碼時,看到大量的__inline__曾經疑惑不已,不知道為什麼Linus在91年就能用上了如此先進的語言功能,後來才知道,這是GNU的擴充關鍵字。
C語言的標準有C89和C99,使用GCC的時候甚至要顯示指定-std=c99才能全面支援C99標準,所以在開源界,大家還是喜歡首選GNU的擴充關鍵字。比如__inline__、__complex__和__restrict__。總而言之,C語言標準幹不過GNU擴充。
下面來看看__restrict__的真正含義。還記得CSDN上曾經載過一篇文章《為什麼有些語言會比別的快》,其中提到“很長一段時間,相同的兩個程式在Fortran和C(或者C++)中運行,Fortran會快一些,因為Fortran的最佳化做的更好。這是真的,就算C語言和Fortran的編譯器用了相同的代碼產生器也是一樣。這個不同不是因為Fortran的某種特性,事實上恰恰相反,是因為Fortran不具備的特性。”這是因為C語言中的指標給編譯器的最佳化帶來了困難,文章中繼續說道:“問題就來了。這些指標可以代替任何記憶體位址。更重要的是,他們可以重疊。輸出數組的記憶體位址也可以同時是輸入數組的。甚至可以部分重疊,輸出數組可以覆蓋一個輸入數組的一半。這對編譯器最佳化來說是個大問題,因為之前基於數組的最佳化不再適用。特別的,添加元素的順序也成問題,如果輸出重疊的數組,計算的結果會變得不確定,取決於輸出元素的動作是發生在元素被覆蓋之前還是之後。”
有了__restrict__,C語言的該問題將不複存在。用__restrict__修飾一個指標後,①該指標只能在定義的時候被初始化;②不會再有別的指標指向該指標指向的記憶體,因此編譯器可以對演算法進行最佳化。如下代碼:
int * __restrict__ p = (int*)malloc(100*sizeof(int));
指標p有__restrict__關鍵字修飾,所以它只能在定義的時候被初始化,以後不能賦值,而沒有__restrict__修飾的指標,可以隨時賦值,如下:
int arr[100];int* pArr;pArr = arr;
指標pArr沒有被__restrict__關鍵字修飾,所以可以將數組的首地址賦值給它。
比如我們定義一個函數對兩塊資料進行操作,結果放入第3塊記憶體,如下:
void func1(void* p1, void* p2, void* p3, int size){ for(int i=0; i<size; i++){ p3[i] = p1[i] + p2[i]; }}
很顯然,由於編譯器沒辦法判斷指標p1、p2、p3指向的記憶體是否重疊,所以無法進行最佳化,加上__restrict__關鍵字後,如下:
void func1(void* __restrict__ p1, void* __restrict__ p2, void* __restrict__ p3, int size){ for(int i=0; i<size; i++){ p3[i] = p1[i] + p2[i]; }}