我們的
PSPDFKit
專案超過 60 萬行程式碼,並且程式碼量還在增長。儘管我們致力於寫簡潔而高效的程式碼,但是這個專案很大,而且有許多邊界情況需要尤其注意。在 PSPDFKit 5 for iOS 專案上,編譯時間尤其成為一個令人頭痛的問題:每次編譯都很慢。
我們的安卓 SDK 也有同樣的問題,幾個月前我們的安卓負責人在技術棧中引入了 ccache 來處理冗長的 C++ NDK 編譯時間,我也是從那個時候開始接觸 ccache。
ccache 是個啥?
ccache 是一個編譯快取器,它會在實際編譯之前先檢查快取。它有直接和預處理模式,而且由於在 Clang 3.2
版本之前是不支援 ccache
外掛,所以在 Clang 3.2
之前會有一些問題,但是現在 Clang
的版本是 3.2.3,所以沒有 Clang
不支援的問題。ccache
是一個具有悠久歷史的專案,其主要焦點是快速正確。
網上搜到“ccache xcode”的資訊都是過時無效的資訊,經過我快速的嘗試網上的方法,都無法配置好使其正常工作。隨著我們的程式碼庫越來越複雜,同時我們的 Jenkins 工作叢集數也有 10 臺 Mac,現在測試時間從幾乎無法忍受變成了正真無法忍受。在 Twitter 抱怨現在每天的工作就是管理 Jenkins 工作叢集之後,Facebook 的 Christian Legnitto(他之前在 Apple 負責 OS X 版本管理工作)建議我們嘗試 ccache
。
Let’s get started
使用以下命令安裝 ccache
:
brew install ccache
複製程式碼
如果你沒安裝 Homebrew
,請移步這裡,先去安裝 Homebrew
,如果你不想移步,就直接使用以下命令安裝 Homebrew
:
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
複製程式碼
為了讓 Xcode
呼叫 ccache
,我們需要一個小指令碼來配置一些環境變數,然後再呼叫 ccache
。將這個指令碼儲存到您專案的某個地方,並將其命名為 ccache-clang
。
#!/bin/sh
if type -p ccache >/dev/null 2>&1; then
export CCACHE_MAXSIZE=10G
export CCACHE_CPP2=true
export CCACHE_HARDLINK=true
export
CCACHE_SLOPPINESS=file_macro,time_macros,include_file_mtime,include_file_ctime,file_stat_matches
exec ccache /usr/bin/clang "$@"
else
exec clang "$@"
fi
複製程式碼
根據你的具體情況,如果你的專案中有 C++
的檔案,你可能還需要一個命名為 ccache-clang++
的指令碼,並在這個指令碼里這麼寫:
#!/bin/sh
if type -p ccache >/dev/null 2>&1; then
export CCACHE_MAXSIZE=10G
export CCACHE_CPP2=true
export CCACHE_HARDLINK=true
export
CCACHE_SLOPPINESS=file_macro,time_macros,include_file_mtime,include_file_ctime,file_stat_matches
exec ccache /usr/bin/clang "$@"
else
exec clang++ "$@"
fi
複製程式碼
這樣看起來是不是有點複雜,如果沒有命中快取,那麼將會按照之前的編譯方式一樣編譯,而不是報 ccache not found
(找不到快取)的錯誤(ccache
內建 shell
指令碼,所以檢查快取很迅速)。
建立 shell 指令碼方法:
建立 touch ccache-clang
開啟指令碼 open -a xcode ccache-clang
貼上指令碼內容
執行指令碼 chmod 755 ccache-clang
複製程式碼
如果去學習 ccache
的配置,你會發現有很多選項可選。上面我們使用的是一種相當激進的快取策略,同時執行良好。對於你自己的專案,你可能在沒有 CCACHE_SLOPPINESS
的情況下開始,然後在一切執行良好的情況下一次性新增快取。
這裡最重要的引數是 CCACHE_CPP2
,這個引數用於解決 Clang
將處理前處理器的檔案輸出,並可能會發現許多你沒有注意到的潛在問題,例如由於巨集擴充套件導致的不必要的括號。使用此選項會稍微減慢編譯時間,但是要比完全沒有使用 ccache
要快得多。Peter Eisentraut 寫了一篇關於這個問題的好文章。
您還需要在 Xcode
中定義 CC
變數。在 PSPDFKit
中,我們在 .xcconfig
檔案中執行此操作,這個檔案在我們所有專案中共享(這是一個很好的統一的專案配置,和易於閱讀和查詢)。同時,您可以直接在 Xcode
專案設定內配置:
CC = "$(SRCROOT)/../Resources/ccache-clang"
複製程式碼
就這麼多了!下次編譯的時候會比正常慢一點,你可以在終端中使用 ccache -s
來檢視 ccache
是否正常工作。剛開始時應該有很多快取沒有命中,但是當快取開始漸漸替代之後的編譯時,編譯速度將會變得快起來。
坑來了
路不平的地方就有坑:ccache
有一些缺點。
不支援 Clang
的 modules
,如果檢測到 -fmodules
, ccache
就會失效。因此,為了相容 ccache
,你需要用老舊的 # import <UIKit/UIKit.h>
替換你專案中所有優雅的 @import UIKit
,以及所有使用 ccache
帶來的問題,比方說巨集的問題。在 PSPDFKit
專案中我們採用了 Objective-C++
的形式,當我們使用很多 C++
程式碼時,就無法使用 modules
了,所以這一點(ccache
不支援 modules
)並沒有影響到我們。 modules
會自動連結用到的 framework
,但是在禁用了 modules
以後,你需要手動新增用到的 framework
,這個工作很無趣,但是也很快就做完。
還需要停止使用 .pch
。蘋果不推薦使用 .pch
,而且一般認為使用 .pch
是不好的程式設計風格,哪裡用到就在哪裡匯入會比 .pch
要好。對我們而言,刪除那些 .pch
還是很容易的。當然,ccache
沒法幫你快取 Swift
檔案。雖然 Swift
也使用 Clang
,但是ccache
對 Swift
檔案束手無策。也許 ccache
最終會支援 Swift
,但我指望不上。因為 Swift
至今沒有穩定,甚至我們要在 Swift
的兩個版本之間做二進位制相容,我們沒法用 Swift
來編寫我們的 SDK,所以 ccache
不支援 Swift
的問題,對我們不是問題。
在編譯期間,我們應該隨時監視專案是否丟擲不相容的警告。請參閱“不支援的編譯器”選項。我花了相當一部分時間去處理這些不相容的問題。設定 CCACHE_LOGFILE
臨時環境變數將有助於我們精確定位錯誤:ccache
將會提示那些標識是有問題的,以及快取命中和未命中的具體情況。
steipete@steipete-rmbp ~ $ ccache -s
cache directory /Users/steipete/.ccache
primary config /Users/steipete/.ccache/ccache.conf
secondary config (readonly) /usr/local/Cellar/ccache/3.2.3/etc/ccache.conf
cache hit (direct) 42530
cache hit (preprocessed) 18147
cache miss 28379
called for link 1344
called for preprocessing 645
compile failed 1
preprocessor error 2
can't use precompiled header 2567
unsupported source language 12
unsupported compiler option 11564
no input file 2
files in cache 124223
cache size 8.7 GB
max cache size 15.0 GB
複製程式碼
搞這個值不值?
給你說一下我們使用的情況,使用了 ccache
以後,我們的編譯執行時間平均為 8 分鐘,之前我們沒有用 ccache
的時候是 14 分鐘。使用 ccache
之前在最快的 MacBook Pro 上編譯打包整個 PSPDFKit
需要 50 分鐘,使用了之後,時間為 15 分鐘。新增 ccache
到我們的技術棧是一個巨大的進步,真後悔我沒有早點知道這個那麼棒的工具!
Precompiled Header 問題
Anton Bukov 說通過禁用 GCC_PRECOMPILE_PREFIX_HEADER
,開啟 GCC_PREFIX_HEADER
的方式來處理這個問題。