減少基於 C/C++ 的系統的編譯時間是所有發佈和編譯工程師所面對的主要挑戰之一。本文研究一些可通過並行活動來加快編譯過程的開源工具選項:將編譯過程分佈到本地網絡中的多臺機器上。本文中的討論主要集中於 GNU make
,因爲它使用比較廣泛。
默認情況下,make
是一個順序工作的工具。它按次序調用底層編譯器來編譯 C/C++ 源。通常,C/C++ 源文件(通常帶有 .cpp/.cxx 擴展名)不需以對方爲基礎即可編譯。使用 –j
選項調用 make
來完成該操作。清單
1 顯示的是一種典型的用法。
清單 1. 典型的 GNU make 調用
make –j10 –f makefile.x86_linux |
–j -- 10
的參數是編譯過程開始後能同時進行的最大編譯數。如果沒有給 -j
提供任何參數,則所有源文件都會在系統中排隊,等待同時編譯。在運行多核系統上的編譯時,使用 -j
選項特別有用。要使用 -j
選項,必須先解決幾個關鍵問題;這些問題將在下面部分討論。
首先要檢查系統配置。在低內存(<512MB RAM)系統上,同時編譯的數量太多會因爲分頁而使系統變慢。在這種情況下會增加編譯時間。您需要進行試驗以得出系統的最佳 -j
值。另一種選擇是使用 GNU make
工具的 –l
或 –load-average
選項,同時也使用 -j
(它只在系統負載小於一定水平時才觸發作業)。
還可以使用同一個臨時文件進行獨立編譯。請考慮清單 2 中所示的 make
代碼片段。
清單 2. 使用同一個臨時文件 y.tab.c 的 makefile。
my_parser : main.o parser1.o parser2.o g++ -o $* $> parser1.o : parser1.y yacc parser1.y g++ -o $* -c y.tab.c parser2.o : parser2.y yacc parser2.y g++ -o $* -c y.tab.c |
假設語法文件 parser1.y 和 parser2.y 位於同一目錄中。在有序編譯期間,yacc(其中 y.tab.c 是默認文件名)爲 parserl 生成文件 y.tab.c,然後爲 parser2 生成文件 y.tab.c;但在並行模式下,這會導致衝突。有幾種方法可以解決這個問題:將兩個 yacc 文件放在單獨的文件夾中;或者使用 –b
選項生成兩個不同的 C 輸出,如清單 3 所示。
清單 3. 使用 yacc 的 –b 選項生成唯一的文件名
parser1.o : parser1.y yacc parser1.y –b parser1 g++ -o $* -c parser1.tab.c |
您必須嚴密監視 makefile 是否發生這種情況,在這種情況下,在順序模式下良好編譯的腳本會在並行模式下出現混亂。
一些 makefile 規則具有隱式依賴項。請考慮清單 4 中的情況,其中一個 Perl 腳本生成一個被其他源包含的頭。
清單 4. 具有隱式依賴項 makefile
my_exe: info.h test1.o test2.o g++ -o $@ $^ test1.o: test1.cxx g++ -c $< test2.o: test2.cxx g++ -c $< info.h: make_header #shell script that generates the header file |
info.h 頭被 test1.cxx 和 test2.cxx 包含。在次序編譯模式下,make
從左到右工作,首先生成文件 info.h。但是,在並行編譯模式下,make
可以自由並行處理所有依賴項 ——如果 info.h 沒有在 test1.cxx 和/或 test2.cxx 編譯開始之前生成的話,這可能導致間歇性編譯失敗。要修復此問題,需要將
info.h 從依賴項列表中刪除,並將它放在 test1.o 和 test2.o 的依賴項列表中。另外,最好使用另一個包裝器來確保 info.h 只生成一次。清單 5 顯示了修改後的 make_header 腳本,而清單 6 顯示了 makefile。
清單 5. 修改 make_header 腳本防止多次編寫
#!/usr/bin/bash if [ -f info.h ] then exit fi echo "#ifndef __INFO_H" > info.h echo "#define __INFO_H" > > info.h echo "#include <iostream>>" > > info.h echo "using namespace std;" > > info.h echo "int f1(int);" > > info.h echo "int f2(int);" > > info.h echo "#endif" > > info.h |
清單 6. 修改後的清單 4 中的 makefile
my_exe: info.h test1.o test2.o g++ -o $@ $^ test1.o: test1.cxx info.h g++ -c $< test2.o: test2.cxx info.h g++ -c $< info.h: make_header #shell script that generates the header file |
一般而言,如果正確創建 makefile,make
-j
就能夠提取充足的並行項。儘量避免在 makefile 中引入不必要的依賴項。
注意,GNU make
只能提取單臺機器的並行項。下一部分將介紹 distcc
,這是一個用於在多臺機器上共享編譯過程的工具。
distcc
工具可以將 C/C++ 代碼的編譯分佈到多臺機器。但這些機器都必須安裝 distcc
。下面是關於快速安裝和配置的說明:
-
下載
distcc
(請參閱 參考資料 部分)。 -
通過執行
./configure; make && make install
在所有機器上編譯distcc
源。 -
編譯過程先在某臺機器上開始,然後分佈所有其他機器(服務器)。在所有服務器中,啓動 distccd 守護程序(您必須具有執行操作的根特權)。distccd 位於 /etc/init.d 文件夾。在根模式下啓動 distccd 的語法是
tcsh-arpan# /etc/init.d/distccd start
在用戶模式下啓動它的語法是tcsh-arpan$ sudo /etc/init.d/distccd
還可以通過運行distccd –daemon –j N
在用戶模式下運行distcc
守護進程,其中N
是您要在給定機器上運行的作業數。 -
本地機器需要知道應該將編譯過程分佈到哪些服務器。根據您的 shell,發出與下面命令相似的命令:
export DISTCC_HOSTS='localhost tintin asterix pogo'
tintin、asterix 和 pogo 是網絡中可以駐留編譯過程的其他主機;localhost
是本地計算機。 -
也可以不使用導出指令。您可以創建一個名爲 hosts 的文件,將服務器的名稱放在該文件中,各個名稱使用空格分隔。將該文件複製到 $HOME/.
distcc
文件夾。
安裝 distcc
之後,惟一需要做的就是觸發編譯。下面是調用方法:
make –j4 CC=distcc –f makefile.x86_linux |
要使 distcc
爲您工作,必須記住以下幾件事情:
- 幾臺機器必須具有一致的配置。這意味着所有機器上必須安裝相同版本的 g++ 編譯器,以及相關的編譯工具,如 ar、ranlib、libtool 等。操作系統的類型和版本也應該相同。
-
在客戶端機器上,
distcc
將預處理代碼發送給服務器機器。您需要驗證distccd
守護進程正在服務器機器上運行。 -
默認情況下,
distcc
在單臺機器上調度的作業數是 CPU 的個數 + 2。對於單核機器,這個數是 3。在觸發進程時請記住這一點:像make –j10 CC=distcc
這樣的命令行(其中只有三個主機)意味着最初觸發 9 個編譯作業。 - 保證底層機器可以訪問存儲源文件的必備文件系統。在基於網絡文件系統(Network File System,NFS)的系統中,一些源區域不能被掛載,這將導致編譯失敗。同時還要仔細監視網絡堵塞。
-
distcc
用於通過網絡編譯源代碼。鏈接步驟可能不是並行的。
distcc
安裝有一個稱爲 distccmon-text 的基於控制檯的監視工具。在啓動編譯過程之前,有必要打開一個單獨的終端窗口併發出 distccmon-text 5。然後,這個終端每隔 5 秒鐘就顯示網絡中多個節點的編譯狀態。清單 7 顯示了一個監視窗口示例。
清單7:distccmon-text 的輸出
2167 Compile memory.c tintin[0] 2164 Compile main.cxx tintin[1] 2192 Compile ui_tcl.cxx asterix[0] 2187 Compile traverse.c asterix[1] 2177 Compile reports.cxx pogo[0] 2184 Compile messghandler.c pogo[1] 2181 Compile trace.cpp localhost[0] 2189 Compile remote.c localhost[1] |
通常,當在 C/C++ 開發框架中修改頭文件時,一般基於 make
的系統最終會重新編譯所有源文件。通常,頭文件更改只會影響源文件的子集,因此不需要進行耗時的編譯清理。還可以使用 ccache
,這個工具能大大減少編譯清理時間(減少到原來的五分之一至十分之一)。
ccache
用作編譯器的緩存。它的工作方式是:從預處理源代碼和用於編譯源代碼的編譯器選項創建一個哈希表。在重新編譯時,如果 ccache
未在預處理源代碼和編譯器選項中檢測到任何更改,它就檢索以前編譯輸出的緩存副本。這有助於加快編譯過程。
要下載 ccache
的最新版本(2.4),請參考 參考資料 小節。轉到 ccache
目錄後,發出命令 ./configure
–prefix=/usr/bin
,接着發出命令 make && make install
。如果 ccache
沒有安裝在 /usr/bin,則檢查 ccache
的位置是否定義爲 PATH
環境變量的一部分。
下面是一些可用於自定義 ccache
安裝的環境變量:
-
CCACHE_DIR
—— 指定ccache
存儲預編譯輸出的文件夾。如果沒有定義這個變量,那麼緩存輸出會默認存儲在 $HOME/.ccache 中。 -
CCACHE_TEMPDIR
—— 指定放置ccache
生成的臨時文件的文件夾。如果沒有定義這個變量,那麼默認使用 $HOME/.ccache。最好定義這個變量和CCACHE_DIR
—— 大多數組織有一個針對特定文件系統區域的用戶配額,如果$HOME
屬於這個區域,那麼配額很快就會用完。顯式地設置這個緩存區域以避免此類問題。 -
CCACHE_DISABLE
—— 設置這個選項告訴ccache
完全調用編譯器,從而繞過緩存。這在診斷時使用。 -
CCACHE_RECACHE
—— 設置這個選項告訴ccache
忽略緩存中現有的條目並調用編譯器;但對於新的條目,則緩存結果。這在診斷時使用。 -
CCACHE_LOGFILE
—— 設置這個選項告訴ccache
隨機記錄該文件在緩存中的統計信息。這對診斷特別有用。 -
CCACHE_PREFIX
—— 向ccache
用於完全調用編譯器的命令行添加一個前綴。這專門用於將distcc
和ccache
連接起來。下一部分將會對此進行詳細討論。
使用 ccache
時,可以帶有 distcc
,也可以不帶。這不依賴於 -j
makefile 選項。ccache
最簡單的用法如下:ccache
g++ -o <executable name> <source file(s)>
。當它與 makefile 一起使用時,就會覆蓋 CC
變量;如清單 8 所示。
清單 8. 使用 CC 變量的示例 makefile
CC := g++ app1: placer1.o route1.o floorplan1.o $(CC) –o $* $^ placer1.o: placer1.cxx $(CC) –o $* -c $< … |
使用清單 8 中的 makefile,發出 make
的語法是 make "CC=ccache g++"
。
爲了同時使用 ccache
和 distcc
,需要將 CCACHE_PREFIX
環境變量設置爲 distcc
,如下所示:export
CCACHE_PREFIX=distcc
(這個語法適用於 bash shell。如果使用另一種 shell,請相應地修改語法)。
下面是一個使用 ccache
和 distcc
的 make
調用示例:
export CCACHE_PREFIX=distcc; make "CC=ccache g++" –j4 –f makefile.x86 |
在編譯過程中,shell 提示符下的實際調用類似於:ccache distcc –o placer1.o –c placer1.cxx
。注意,只需在本地機器上安裝 ccache
。ccache
進行第一次檢查,確定副本是否存在本地緩存中;如果不存在,就由 distcc
進行分佈式編譯。
本文探討了 GNU make
、distcc
和 ccache
,這些工具能夠並行分佈編譯過程。它們還有幾個可以進一步自定義的其他特性 —— 例如,ccache
有一個限制緩存大小的 –M
選項;distcc
有一個基於
GUI 的監視器 distcc
-gnome,它會跟蹤網絡編譯活動(如果使用 –use-gtk
選項編譯 distcc
,就會創建該監視器)。參考資料 部分中的鏈接提供更加詳細的信息。