[Happy Coding] 加速Windows GUI debug版本的編譯

1. 問題描述
我們重構我們的GUI程序時,增加了很多小的工程庫,VC++編譯GUI最頂層DLL庫libpkgA的速度讓人幾乎無法忍受。
以下是從我們的buildbot系統裏截取出來的LOG:

28>...
28>Embedding manifest...
28>Build Time 188:17

編譯時間**188分鐘**。

GUI app依賴於這個DLL庫,同時也依賴其它一些庫,編譯它的LOG如下:

79>...
79>Embedding manifest...
79>Build Time 157:32

又需要**157分鐘**。

如果算上編譯其它子庫的時間(~37mins),那麼編譯GUI app所需要的時間爲 382mins (6.4hours)。
我想,clean build DEBUG GUI app需要6.4個小時的時間幾乎是無法忍受的。

**如何加速這個編譯過程加速?**

1) Google? 當你輸入"VC compile link improve"這類關鍵字時,得到可能會有參考價值的結果可能有:
a. http://blogs.msdn.com/b/vcblog/archive/2010/04/01/vc-tip-get-detailed-build-throughput-diagnostics-using-msbuild-compiler-and-linker.aspx
b. http://blogs.msdn.com/b/vcblog/archive/2009/09/10/linker-throughput.aspx
c. http://stackoverflow.com/questions/143808/how-to-improve-link-performance-for-a-large-c-application-in-vs2005

2) 到SO提問,我想我們還沒搞清楚爲什麼編譯慢,那個環節慢,沒頭沒腦去提問,估計也得不到很好的結果。

面對如此情況,我們只能硬着頭皮上,嘗試着先把問題分析下,認識清楚我們所面對的問題。

2. 問題分析
以下分析是以編譯GUI app的問題爲主:
1) 找到以前編譯GUI app成功的buildlog.htm文件,進行分析
下面是以前編譯GUI app時在項目的臨時目錄中產生的buildlog.htm的記錄:

> Creating command line """e:\GUIApp\Build-vc90\tachyon\Debug\BAT00001811168124.bat"""
Creating temporary file "e:\GUIApp\Build-vc90\tachyon\Debug\RSP00001911168124.rsp" with contents
[
/Od /I "D:\wxWidgets-2.8.9\src" /I "D:\wxWidgets-2.8.9\include" /I "D:\wxWidgets-2.8.9\contrib\include" /I "E:\GUIApp\third_party\wxWidgets-2.8.9\lib\vc_lib\mswd" /I "E:\GUIApp\third_party\propgrid\\include" (more options)
]
Creating command line "cl.exe @"e:\GUIApp\Build-vc90\tachyon\Debug\RSP00001911168124.rsp" /nologo /errorReport:prompt"

可以看到VC會在編譯(compile,調用cl.exe)之前先推導出該庫所需要的編譯選項,保存到一個臨時文件中,之後以這個臨時文件作爲輸入啓動cl.exe編譯項目的代碼。

> Creating temporary file "e:\GUIApp\Build-vc90\tachyon\Debug\RSP0000131924013376.rsp" with contents
[
/OUT:"..\..\Build-vc90\bin\Debug\Tachyon.exe" /INCREMENTAL:NO /LIBPATH:"E:\GUIApp\third_party\TachyonGUI-Libs\vc90\Debug" /LIBPATH:"..\..\Build-vc90\bin\Debug" /LIBPATH:"E:\GUIApp\third_party\TachyonGUI-Libs\vc90\DLL_Debug\wxlib28" /LIBPATH:"E:\GUIApp\third_party\BugTrap\Win32\Bin\\" /LIBPATH:"E:\GUIApp\third_party\win32\protobuf\2.3.0\dll32" /MANIFEST /MANIFESTFILE:"..\..\Build-vc90\tachyon\Debug\Tachyon.exe.intermediate.manifest" /MANIFESTUAC:"level='asInvoker' uiAccess='false'" /DEBUG /PDB:"e:\GUIApp\Build-vc90\bin\Debug\Tachyon.pdb" /SUBSYSTEM:WINDOWS /DYNAMICBASE /NXCOMPAT /MACHINE:X86 libexpatd.lib libpkgPlatform.res odbc32.lib odbccp32.lib comctl32.lib rpcrt4.lib wsock32.lib libprotobuf.lib winmm.lib wxmsw28d.lib wxmsw28d_stc.lib wxexpatd.lib wxPlotd.lib wxpngd.lib wxzlibd.lib wxjpegd.lib wxtiffd.lib xerces-c_2d.lib lua51d.lib libeay32d.lib libexpatd.lib ssleay32d.lib lokisd.lib BugTrap.lib log4cplusD-1_0_4.lib wxcode_msw28d_propgrid.lib wxCode_msw28d_treectrl.lib xrc.lib chartdir50.lib zlibwapi.lib cairo.lib kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib "..\..\build-vc90\bin\debug\libpkgA.lib" "..\..\build-vc90\bin\debug\libD.lib" "..\..\build-vc90\bin\debug\libC.lib" "..\..\build-vc90\bin\debug\libD.lib" "..\..\build-vc90\bin\debug\libE.lib" "..\..\buil
"..\..\Build-vc90\tachyon\Debug\GUIApp.obj"
"..\..\Build-vc90\tachyon\Debug\A.obj"
"..\..\Build-vc90\tachyon\Debug\B.obj"
"..\..\Build-vc90\tachyon\Debug\main.obj"
]
Creating command line "link.exe @"e:\GUIApp\Build-vc90\tachyon\Debug\RSP0000131924013376.rsp" /NOLOGO /ERRORREPORT:PROMPT"

可以看到VC會在鏈接(link,調用link.exe)之前先推導出該庫所需要的編譯選項,保存到一個臨時文件中,之後以這個臨時文件作爲輸入啓動link.exe進行鏈接。

需要注意的是,推導依賴庫並創建編譯和鏈接選項的2個臨時文件,是在編譯之前做的。這樣帶來一個問題,就是如果庫的依賴非常複雜,VC的推導過程將會變得非常慢。
2) 開啓任務管理器觀察VC++(devenv.exe)進程的活動狀態

你不可能傻傻的坐在電腦前等着微軟的VC進程恢復到正常狀態,於是你去看會兒書,瀏覽下今天的新聞,又或者去刷刷火車票,在你做完這些之後,回到VC界面,然後發現,我靠,還是沒有任何LOG輸出。你毅然果然的Kill devexe進程。然後重新打開solution文件,開啓Linker選項中的"Show Progress"選項:Display All Progress Messages (/VERBOSE),又啓動app的build。去看部電影再回來。。。

回來之後,你可能會看到下面類似的輸出:

> 28>Linking...
28>Starting pass 1
28>Processed /DEFAULTLIB:oleacc
28>Processed /DEFAULTLIB:msvcprtd
28>Processed /DEFAULTLIB:uuid.lib
28>Processed /DEFAULTLIB:libboost_regex-vc90-mt-gd-1_37.lib
28>Processed /DEFAULTLIB:libboost_signals-vc90-mt-gd-1_37.lib
28>Processed /DEFAULTLIB:MSVCRTD
28>Processed /DEFAULTLIB:OLDNAMES
28>AppBase.obj : warning LNK4075: ignoring '/EDITANDCONTINUE' due to '/INCREMENTAL:NO' specification
28>Searching libraries
28>    Searching ..\..\Build-vc90\bin\Debug\xrc.lib:
28>      Found "void __cdecl ui_xrc::InitXmlResource(void)" (?InitXmlResource@ui_xrc@@YAXXZ)
28>        Referenced in AppCommonObjMgr.obj
28>        Loaded xrc.lib(tmp_init.obj)
28>      Found "void __cdecl xml_setting_dlg_init(void)" (?xml_setting_dlg_init@@YAXXZ)
28>        Referenced in xrc.lib(tmp_init.obj)
28>        Loaded xrc.lib(xml_setting_dlg.obj)
28>      Found "void __cdecl verify_setup_tflex_submask_editor_init(void)" (?verify_setup_tflex_submask_editor_init@@YAXXZ)
28>        Referenced in xrc.lib(tmp_init.obj)

可以看到VC會自動從推導出的依賴庫中嘗試resolve所有未解決的符號。

3. 方案提出
1) 將libpkgA動態庫編譯改成靜態庫編譯
之前問題中,不僅GUI app編譯器會慢,libpkgA DLL庫編譯也很慢,時間上不相上下。爲什麼編譯libpkgA DLL庫也這樣慢呢?
要知道libpkgA DLL是配置成依賴於其它靜態庫的,我們知道靜態庫可以看成是obj文件的打包集合,當編譯這個DLL庫的時候,VC便會根據項目依賴庫列表自動推導所有的依賴庫,而且是遞歸的,因爲被依賴的靜態庫依賴其它靜態庫,後者的符號並沒有進入到前者的LIB文件中,所以DLL庫編譯只能遞歸去查找所有的靜態LIB庫。像libpkgA依賴的庫非常龐雜,VC遞歸的推導將會非常慢。
改成靜態庫編譯之後,僅僅只是許多obj文件的打包操作,故可以非常快。
同時改成靜態庫之後,也避免了相同的符號在最終的app exe運行加載這個DLL後中存在兩份的可能性,因爲app exe也可以鏈接那些靜態庫。

2) 顯示的告訴VC,一個工程所依賴的庫
對於我們自己編寫的依賴庫,我們通常不會顯示的寫到項目的編譯選項中(VC的項目linker頁面input欄),通常會將一些外部依賴的第三方庫寫在那裏,因爲那些第三方庫不會頻繁的更改。我們會通常GUI界面去設置我們編寫的庫的相互依賴性(Project/Project Dependencies),這樣當庫遞歸依賴很複雜時,就帶來前面VC自動推導很慢的問題了。
一種方案是我們可以將自己編寫的依賴庫顯式寫到項目的編譯選項中,另一種方案就是在代碼中寫如下的pragma指示編譯器去依賴某個庫:
#pragma comment(lib, "xxx.lib")
 
3) 建立一個dummy工程(prebuild)來觸發依賴庫的預編譯
顯式告訴VC編譯器去依賴某個外部庫,而不讓它自己去推導,會帶來一問題就是,當我們編譯工程時,VC自己不會先去編譯那些顯式依賴的庫(它會認爲它們已經準備好了),它也不知道該怎麼去編譯它們,故如果某個外部庫沒先編譯好,鏈接工程時就無法打開那個庫的.lib文件。
有什麼方式可以讓VC先去編譯那些外部依賴庫呢?用prebuild命令的方式。
VC提供prebuild命令的機制,允許先運行一個外部命令,然後再編譯工程。當然也支持編譯工程之後再運行一個外部命令,叫做Post-build。
外部命令通常是一個BAT腳本文件,因此可以寫一個BAT腳本來乾點什麼?
寫一個BAT腳本來一個一個的編譯那些外部依賴庫?代碼如下:
    for %%L in (%LIBS%) do (
    echo Building %%L ...
    <<<command to build one lib %%L>>>
    )

編譯一個庫工程的編譯命令是什麼?直接google,可以用devenv.com/devenv.exe外部命令帶庫工程的名字辦到(其實devenv.exe是VC的可執行文件),於是寫成下面這樣:?
    for %%L in (%LIBS%) do (
    echo Building %%L ...
    devenv.com "%solution%" /Build "%config%"  /project %%L
    )
試想下,如果依賴的庫工程很多,devenv.com外部命令就需要啓動很多次。我想這樣不太好。
想到一句名言:軟件工程中問題,通常可以引入一個間接層來解決。
如果不斷執行devenv.com命令開銷有點大,可以引入一箇中間依賴庫libprebuild,讓編譯工程prebuild這個libprebuild依賴庫,更重要的一部是讓libprebuild工程依賴所有原先編譯工程依賴的外部庫。
假如編譯工程是app,以前依賴於庫工程libA, libB, ..... libN,現在引入中間依賴庫之後,讓libprebuild工程依賴於libA, libB, ..., libN,然後讓app先執行prebuild命令,編譯libprebuild工程,這樣一來編譯app工程,會先編譯libprebuild工程,編譯之前VC會自動推導出它的依賴庫,然後就先編譯libA, libB, ..., libN。這樣就好像編譯app工程預先編譯libA, libB, ..., libN一樣。
特別需要注意的是,libprebuild庫工程需要編譯成靜態庫,否則又回到老問題上了。

4. 總結
採用了上述方案之後,任何app工程中如果存在編譯變慢的問題(因爲VC龜速的自動的庫推導/展開),編譯採用上述的方案。步驟如下:
1) 在該工程中,增加一個cpp文件,裏面顯示依賴外部庫:
    #pragma comment(lib, "A.lib")
    #pragma comment(lib, "B.lib")
    ...

2) 寫一個prebuild.bat腳本,裏面寫上類似下面的代碼(我知道Window Batch腳本的語法很怪異):
    for %%L in (%LIBS%) do (
    echo Building %%L ...
    devenv.com "%solution%" /Build "%config%"  /project %%L
    )
其中LIBS就是那個創建出來中間依賴庫: set LIBS=libprebuild, config可以是Debug或者其它。

3) 在工程中的Prebuild Event項的Command欄中寫下如下的指令:
    call $(SolutionDir)\libprebuild\prebuild.bat $(ConfigurationName)

4) 最後也是最關鍵的一步: 在Project\Project Dependencies對話框中,取消勾選該工程所有的依賴庫項。

按照上述方法編譯原來的工程,你會得到< 10 mins的編譯時間。我想你肯定開心的笑了。:)

最後說下該方案存在的問題就是:它不能支持VC界面上的Build Only菜單項,因爲不管怎樣,它總是需要去啓動prebuild.bat腳本編譯libprebuild工程。

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