分佈式編譯

減少基於 C/C++ 的系統的編譯時間是所有發佈和編譯工程師所面對的主要挑戰之一。本文研究一些可通過並行活動來加快編譯過程的開源工具選項:將編譯過程分佈到本地網絡中的多臺機器上。本文中的討論主要集中於 GNU make,因爲它使用比較廣泛。

GNU make 中的 –j 選項

默認情況下,make 是一個順序工作的工具。它按次序調用底層編譯器來編譯 C/C++ 源。通常,C/C++ 源文件(通常帶有 .cpp/.cxx 擴展名)不需以對方爲基礎即可編譯。使用 –j 選項調用 make 來完成該操作。清單 1 顯示的是一種典型的用法。


清單 1. 典型的 GNU make 調用
				
make –j10 –f makefile.x86_linux

–j -- 10 的參數是編譯過程開始後能同時進行的最大編譯數。如果沒有給 -j 提供任何參數,則所有源文件都會在系統中排隊,等待同時編譯。在運行多核系統上的編譯時,使用 -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 簡介

distcc 工具可以將 C/C++ 代碼的編譯分佈到多臺機器。但這些機器都必須安裝 distcc。下面是關於快速安裝和配置的說明:

  1. 下載 distcc(請參閱 參考資料 部分)。
  2. 通過執行 ./configure; make && make install 在所有機器上編譯 distcc 源。
  3. 編譯過程先在某臺機器上開始,然後分佈所有其他機器(服務器)。在所有服務器中,啓動 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 是您要在給定機器上運行的作業數。
  4. 本地機器需要知道應該將編譯過程分佈到哪些服務器。根據您的 shell,發出與下面命令相似的命令:
    export DISTCC_HOSTS='localhost tintin asterix pogo'
    

    tintin、asterix 和 pogo 是網絡中可以駐留編譯過程的其他主機;localhost 是本地計算機。
  5. 也可以不使用導出指令。您可以創建一個名爲 hosts 的文件,將服務器的名稱放在該文件中,各個名稱使用空格分隔。將該文件複製到 $HOME/.distcc 文件夾。

distcc 的工作原理

distcc 將預處理代碼發送給網絡中的其他指定機器。distccd 守護進程確保編譯在遠程機器上發生。distcc 的設計目的是與 GNU make 的並行編譯(-j)選項一起使用。distcc 本身不是一個編譯器;它只是用作 g++ 的一個前端。幾乎 g++ 的所有選項都可以按原樣傳遞給distcc

安裝 distcc 之後,惟一需要做的就是觸發編譯。下面是調用方法:

make –j4 CC=distcc –f makefile.x86_linux

使用 distcc 需要記住的幾個關鍵點

要使 distcc 爲您工作,必須記住以下幾件事情:

  • 幾臺機器必須具有一致的配置。這意味着所有機器上必須安裝相同版本的 g++ 編譯器,以及相關的編譯工具,如 ar、ranlib、libtool 等。操作系統的類型和版本也應該相同。
  • 在客戶端機器上,distcc 將預處理代碼發送給服務器機器。您需要驗證 distccd 守護進程正在服務器機器上運行。
  • 默認情況下,distcc 在單臺機器上調度的作業數是 CPU 的個數 + 2。對於單核機器,這個數是 3。在觸發進程時請記住這一點:像 make –j10 CC=distcc 這樣的命令行(其中只有三個主機)意味着最初觸發 9 個編譯作業。
  • 保證底層機器可以訪問存儲源文件的必備文件系統。在基於網絡文件系統(Network File System,NFS)的系統中,一些源區域不能被掛載,這將導致編譯失敗。同時還要仔細監視網絡堵塞。
  • distcc 用於通過網絡編譯源代碼。鏈接步驟可能不是並行的。

必須按次序運行的編譯過程

編譯中的有些步驟可能不是並行的 —— 必須在單臺機器上才能使用腳本生成某些頭、鏈接等。要更好地處理這種情況,最好將原始 makefile 拆分爲多個 makefile,明確劃分哪些可以並行運行,哪些不可以,然後按以下方式運行它們: tcsh-arpan$ make –f make.init; make CC=distcc –j4 –f make.compile_x86; make –f make.link

監視 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]

使用 ccache 進一步提高編譯速度

通常,當在 C/C++ 開發框架中修改頭文件時,一般基於 make 的系統最終會重新編譯所有源文件。通常,頭文件更改只會影響源文件的子集,因此不需要進行耗時的編譯清理。還可以使用 ccache,這個工具能大大減少編譯清理時間(減少到原來的五分之一至十分之一)。

ccache 用作編譯器的緩存。它的工作方式是:從預處理源代碼和用於編譯源代碼的編譯器選項創建一個哈希表。在重新編譯時,如果 ccache 未在預處理源代碼和編譯器選項中檢測到任何更改,它就檢索以前編譯輸出的緩存副本。這有助於加快編譯過程。

安裝 ccache

要下載 ccache 的最新版本(2.4),請參考 參考資料 小節。轉到 ccache 目錄後,發出命令 ./configure –prefix=/usr/bin,接着發出命令 make && make install。如果 ccache 沒有安裝在 /usr/bin,則檢查 ccache 的位置是否定義爲 PATH 環境變量的一部分。

Ccache 環境變量

下面是一些可用於自定義 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

使用 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。注意,只需在本地機器上安裝 ccacheccache 進行第一次檢查,確定副本是否存在本地緩存中;如果不存在,就由 distcc 進行分佈式編譯。

結束語

本文探討了 GNU makedistcc 和 ccache,這些工具能夠並行分佈編譯過程。它們還有幾個可以進一步自定義的其他特性 —— 例如,ccache 有一個限制緩存大小的 –M 選項;distcc 有一個基於 GUI 的監視器 distcc-gnome,它會跟蹤網絡編譯活動(如果使用 –use-gtk 選項編譯 distcc,就會創建該監視器)。參考資料 部分中的鏈接提供更加詳細的信息。

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