淺談GCC預編譯頭技術

原文:http://blog.csdn.net/wallwind/article/details/7676019

文/jorge

——謹以此文,悼念我等待MinGW編譯時逝去的那些時間。

其 實剛開始編程的時候,我是絲毫不重視編譯速度之類的問題的,原因很簡單,因爲那時我用BASICA。後來一直用到C++ Builder,儘管Borland的廣告無時無刻不在吹噓其編譯速度,我卻從沒有對這個問題上心過,因爲心裏根本沒有“編譯速度慢”這種概念。沒有壞, 哪來好?所謂矛盾的對立統一。遇到的第一個“慢”的編譯器也許是javac,但因爲Java的特殊性,也就容忍了。真正接觸到世間的“惡勢力”,還要算是 第一次使用GCC的時候……準確地說是MinGW。開源世界曾給我諸多驚喜,其一就是原來編譯器也可以這麼慢的。那時我不禁對開源社區肅然起敬,他們就用 這樣的編譯器,建立起了怎樣一個多彩的世界!也在那時才明白了,Borland其實真的很了不起。

時至今日我也不是很瞭解Borland是 怎麼做到的,很久以來也不知道GCC是差在了哪裏。然而……有一次心血來潮,忽然想看看MinGW編譯過程中加載的所有頭文件。於是用了一下 -H 參數。結果是滿意的,加載的頭文件真多呀。接下來……開始感覺到另外的一些東西了。敢情,大部分編譯時間是浪費在這裏的呀?——“預編譯頭”的概念如鯨魚 般躍出腦海。

預編譯頭技術是在VC中第一次瞭解的,其對編譯速度的提高,絕對給人以深刻的印象。使用MinGW的時候居然忘了這個古老的咒 語。是否正是我所需要的?百度幾下,結果令人失望,這方面的文獻少得可憐,更令人沮喪的是還有不少人相信GCC是沒有預編譯頭技術的。賊心不死的我打開 GCC官方文檔,查找precompiled headers。慢着,居然如此順利!——官方文檔討論篇幅並不長,但足以讓我喊萬歲了~不用多,一句話就夠了,怎麼說來着?Simply compile it!

所謂預編譯頭,就是把頭文件事先編譯成一種二進制的中間格式,供後續的編譯過程使用。不要把這個中間格式與. o/.obj/.a/.lib的格式混淆,他們是截然不同的,所以預編譯頭文件的特性和目標文件也不同(儘管他們都屬於某種中間文件)。——但也有類似的 地方的,比如,它們都是編譯器之間不兼容的^_^,就是說你不能把VC生成的預編譯頭拿到GCC上去用。甚至擴展名都不一樣,VC的是大家都熟悉的. pch,而GCC的,是.gch——今天的主角。

爲什麼要使用預編譯頭?再明確不過了,提高編譯速度。爲什麼會提高編譯速度?這麼說吧,你 有兩個文件a.cpp和b.cpp,都包含了同一個頭文件c.h。那麼正常的流程是:將c.h和a.cpp合併,編譯成a.o;將c.h和b.cpp合 並,編譯成b.o;最後將a.o和b.o鏈接成可執行文件。過程很簡單,浪費時間之處也一目瞭然:頭文件c.h的內容實際上被解析了兩遍。也許你要說,當 然要兩遍了,因爲頭文件幾乎是不生成任何代碼的,只有依附於具體的.cpp文件纔有意義。正確,但那只是在代碼執行過程中。但在代碼編譯的時候呢?編譯器 讀入源代碼,首先將其解析成爲一種內部的表示方式。這個過程與其所依附的.cpp文件並無關係,編譯器接着可以讀入.cpp文件並同樣解析成內部表示,然 後把兩段內部表示拼接起來,再進行接下來的操作。既然編譯兩個.cpp文件都要先對c.h進行解析,那幹嘛不把c.h解析好了保存成臨時文件,用時讀入, 不就可以省了一次解析的時間了嗎?——預編譯頭技術節省時間的原理正在於此,尤其是在這樣一個事實下:對源代碼的“解析”這個步驟,確實是佔了編譯時間中 很可觀的一部分。

我看見你滿是狐疑的臉:預處理,就是編譯之前的處理,合併.h和.cpp文件分明是預處理的步驟,而解析源代碼是編譯之中 的步驟,先解析後合併?怎麼“預”處理反而跑到編譯步驟之後了?這還叫“預”嗎?——這個問題我們決定不深究了,畢竟現在的編譯器早就混淆了預處理與編譯 的界限……畢竟,這麼做是管用的,對嗎?

我們來看看結果。寫一個C++的Hello world,使用cout輸出一行字。包含了什麼頭文件?當然是iostream。這個頭文件對於人們來說,絕對是熟視無睹級別的。然而使用它的時候,你 注意到編譯器幕後的累累“罪行”了嗎?是的,用 -H 參數編譯一下這個Hello world吧!看看總共加載了多少個頭文件?我的機器上,總共103個!

是的,你應該將它們做成一個.gch文件。如何做?如前所述,再簡單不過:只要編譯它就可以了:

g++ xxx.h

一 句話,就是:把.h文件當成.cpp文件一樣來編譯。這是最簡單的,如果需要控制編譯細節,比如常量定義之類,大可加上其它選項。運行之後,你會發現同個 目錄裏生成了一個名叫xxx.h.gch的文件,這就是我們要的。也許你和我一樣,迫不及待地嘗試g++ iostream了?呵呵,結果一定是和我一樣的失敗——在編譯.gch的過程中,GCC並沒有使用環境變量或 -I 選項來查找被編譯的頭文件,被編譯的頭文件必須在當前目錄下。然而,被編譯的頭文件所進一步包含的其它頭文件,卻可以通過以上途徑找到。簡言之,就是把直 接編譯的那個頭文件以類似對待.cpp文件的方式處理了。現在知道該如何編譯iostream了吧?對,在當前目錄裏建立一個頭文件,起個隨你喜歡的名 字,比如foo.h,在其裏面寫上:#include <iostream>,然後編譯它:g++ foo.h。生成的foo.h.gch,就是我們要的了。其它文件需要用到iostream的,不要包含iostream,要包含foo.h。切記,不是 去包含foo.h.gch!

如果你用過VC,那麼這個foo.h也許會讓你找到一種似曾相識的感覺吧?對了,就相當於那個 stdafx.h!那麼你也該記得,每個文件包含這個foo.h,都應該在文件一開始的地方,否則會出錯。真的,終於找到了GCC中的stdafx.h, 這種感覺幾乎讓人熱淚盈眶了^_^

那麼接下來,照搬一些stdafx.h相關的注意事項吧,它們同樣適用於.gch文件:應該把那些不常修 改的(首當其衝,當然是系統的)頭文件放在預編譯頭裏,而那些屬於你的程序的一部分的頭文件,一般並不放在預編譯頭裏,因爲它們可能隨時要被修改的。每修 改一次就要重新生成預編譯頭,並沒有速度優勢可言,失去預編譯頭的意義了。另外重要的注意事項是:如果你生成預編譯頭的時候用了一些選項,比如宏定義,那 麼使用這個預編譯頭的其它源代碼文件,被編譯的時候也要使用這些選項,否則會因爲不匹配而編譯失敗。

對了,說了半天,從來沒有正面講過如何 使用已經生成的預編譯頭。然而看到這裏也該明白了,是的,很簡單,只要包含其所對應的.h文件即可!比如你有個頭文件叫foo.h,另外有一大堆其它文件 都包含了這個foo.h,原來沒有使用預編譯頭技術,現在忽然想使用了,於是把foo.h編譯成了foo.h.gch。那其它文件要做怎樣的修改?——什 麼都不用,一切照舊!聰明的GCC編譯器在查找一個.h文件之前,會自動查找其目錄裏有沒有對應的.gch文件,如有,且可用,則用之;沒有,纔用到真正 的.h頭文件。——慢着,“如有,且可用”,什麼叫“可用”?——就是指這個.gch格式要正確,版本要兼容,而且如上所述,編譯兩者要用同樣的選項。如 果.gch不可用,編譯器會給出一條警告,告訴我們:這個預編譯頭不能用!我只好用原有的.h頭文件啦!什麼?你說看不到這個警告?——當然,要先打開 -Winvalid-pch 選項才行,其默認是關閉的。

用 -H 選項感受一下預編譯頭的清爽吧!再沒有滾不完的頭文件了,明顯提高的速度,絕對會讓你有種翻身解放的感覺,原來MinGW也可以和蝸牛般的速度說再見的。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章