淺談GCC先行編譯頭技術

來源:互聯網
上載者:User

淺談GCC先行編譯頭技術

文/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也可以和蝸牛般的速度說再見的。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.