讓 Arrow C++ 建置更簡單、更小、更快速
已發布 2020年7月29日
作者 Apache Arrow PMC (pmc)
在過去四年半的時間裡,我們致力於為 C++ 中的高效能分析應用程式建構一個「開箱即用」的開發平台。隨著專案範圍的擴大,我們有時會採用額外的函式庫依賴,以支援各種系統和資料處理任務。
雖然這些依賴關係讓我們在解決難題時更有優勢,但在某些情況下,它們也為依賴 Arrow 的專案增加了複雜性。因此,有些專案對於依賴 Arrow C++ 函式庫感到擔憂,特別是當他們對 Arrow 函式庫功能的使用有限時。事實上,在 Arrow 專案開發的早期階段,依賴管理問題確實為早期採用者帶來了一些困擾。
我們希望開發人員能夠信任他們可以使用並依賴我們的函式庫,並且這樣做不會為他們自己的專案維護或使用者增加負擔。在過去一年中,我們進行了許多重要的專案,以適應人們希望依賴 Arrow C++ 的不同方式。我們的目標是讓建置過程預設情況下簡單易用,無需特殊的環境設定,同時也為需要特殊設定的使用者提供高度的可配置性。這包括為希望使用 Arrow C++ 核心但不想承擔任何傳遞依賴關係的專案提供零依賴選項。即使我們持續新增功能,我們也努力使建置過程更快、更精簡。
這篇文章涵蓋了我們在 C++ 函式庫以及依賴於它們的 Arrow Python 和 R 套件中所做的許多努力。與一年前相比,建置體驗在更廣泛的平台上更可靠,所需的依賴關係更少,並且產生的套件更小。
最小化的預設建置選項
對於將 Arrow 作為依賴項的人來說,一個痛點是許多可選的專案組件在建置時預設為啟用,因此需要這些可選組件的額外依賴項。我們沒有期望使用者逐一禁用可選組件,而是將所有可選組件的預設值設定為 OFF
,以便預設配置是一個無依賴的最小核心建置。
預設情況下唯一啟用的第三方函式庫是 jemalloc,這是專案推薦的記憶體分配器(在 Windows 上除外,它也被禁用)。鑑於 Arrow 應用程式通常處理大量資料,我們還發現,使用 jemalloc 和 mimalloc 等專案提供的記憶體分配器,比預設的系統分配器效能更好。即便如此,如果需要,也可以禁用它。
為了展示最小化建置,我們提供了一個 Dockerfile,可用於建置專案,僅需 CMake 和 C++ 編譯器,且零依賴。此外,我們還包含了一個 範例,說明如何在另一個 CMake 專案中將 Arrow 作為外部專案依賴項包含進來。
CMake 中彈性的依賴配置
作為改進我們基於 CMake 的建置系統的一部分,我們使建置依賴項的配置對於不同使用者的需求來說既靈活又一致。在某些情況下,開發人員希望 Arrow 針對外部套件管理器(例如基於 Debian 的 Linux 發行版中的 apt)提供的依賴項進行建置。在其他情況下,開發人員可能希望避免系統函式庫的任何怪異之處,並將所有依賴項與 Arrow 建置一起建置。
對於每個套件,${Library}_SOURCE
CMake 選項可以設定為以下三個值之一
SYSTEM
,當依賴項將從外部提供時(例如由 Linux 發行版或 Homebrew 提供)BUNDLED
,當您希望在建置 Arrow 時從原始碼建置依賴項,然後與結果函式庫靜態連結時AUTO
,它會嘗試SYSTEM
方法,但如果找不到依賴項,則會回退到BUNDLED
我們還為開發人員使用 conda 或 Homebrew 套件管理器時的常見情境提供了 CONDA
和 BREW
來源類型。這些依賴來源可以在個別依賴項的基礎上配置,也可以使用 ARROW_DEPENDENCY_SOURCE
CMake 選項進行全域配置。AUTO
是預設值,它可以在可能的情況下使用預先建置的系統函式庫來加快建置速度,即使系統上沒有所有依賴項,建置仍然可以成功。
減少外部依賴項
另一個關注的領域是審查我們的依賴項。我們仔細檢查並找到了可以刪除外部依賴項的地方,而不會失去有用的功能,也不必重寫大量程式碼或將過多程式碼複製到我們的程式碼庫中。
我們已經消除了 Boost 作為核心 Arrow 函式庫的依賴項,並且在其他組件(Gandiva、Parquet 等)中,Boost 的使用已大大減少。此外,當在 Arrow 建置中「捆綁」建置 Boost 時,我們將下載的 Boost 套件縮減到最低需求,減少了 90% 的下載大小。
我們將一些小的依賴項(例如 double-conversion 和 uriparser 函式庫)納入專案程式碼庫中,這樣它們就不需要單獨下載和建置。
我們還編譯了 Flatbuffers 和 Thrift 定義(分別用於實作 Arrow 和 Parquet 格式),並將產生的 C++ 程式碼簽入到 Arrow 儲存庫中。這意味著 Flatbuffers 不再是 Arrow 的建置或執行時期依賴項,我們只需要 Thrift C++ 函式庫,而不需要 Thrift 編譯器,後者對 flex 和 bison 有額外的依賴關係。
C++ 函式庫大小縮減
隨著 C++ 程式碼庫規模的增長,編譯時間以及 C++ 編譯器產生的二進位碼量也隨之增加。在過去幾個月中,我們開始分析 Arrow 函式庫的編譯時間和產生的程式碼大小。這帶來了顯著的大小縮減(自 0.17.0 以來程式碼大小縮減超過 30%)。我們還重新組織了標頭檔,以避免包含不需要的標頭檔,從而減輕了 C++ 編譯器的負擔並縮短了編譯時間。
Python wheel 檔案
Python 套件索引 (PyPI) 上的二進位 wheel 套件的期望是它們是獨立的,除了其他 Python 套件之外沒有外部依賴項。此外,pyarrow 的每個使用者可能對專案有不同的需求。有些使用者只想讀取 Parquet 檔案並將其轉換為 pandas 資料框,而另一些使用者則希望使用 Flight 來移動大型資料集。因此,從專案一開始,「pyarrow」wheel 就是一個相當全面的建置版本,包含了我們在實務上可以維護的盡可能多的可選組件。
全面的 wheel 套件有一些缺點:對使用者來說最明顯的是它很大。此外,由於與 C++ 共享函式庫相關的問題,在幾個版本中,wheel 套件會在磁碟上建立每個 C++ 函式庫的兩個副本,導致磁碟使用量增加一倍。這為在 AWS Lambda 等空間受限環境中使用 pyarrow 的人們造成了問題。
在 1.0.0 版本中,我們實施了一些變更,將 wheel 檔案的大小(無論是 .whl
格式還是安裝在磁碟上)減少了約 75%。
- 解決了在 site-packages 目錄中建立每個共享函式庫兩個副本的問題。
- 禁用了 Gandiva,它需要 LLVM 執行時期環境,這是最大的靜態連結依賴項。Gandiva 現在仍然可供 conda 使用者使用,只是不包含在 wheel 檔案中,我們希望將來將其封裝為單獨的
pyarrow-llvm
套件。 - 如上所述,縮減了 C++ 共享函式庫的大小。
現在 pyarrow 的大小與 NumPy 差不多,因此 Python 專案可以更輕鬆地將其作為硬依賴項,而無需擔心磁碟上的大小過大。
展望未來,我們已經討論了將 pyarrow 分解為多個 wheel 套件的 策略,有點像「樞紐輻射」模型,其中一些可選部分作為單獨的 wheel 檔案安裝,因此只需要某些「核心」功能的人們只需安裝一個小型套件。然而,這將是一個重大的專案,因此目前我們專注於改進全面的 wheel 套件。
R 套件
為 R 封裝 Arrow 涉及與 Python wheel 檔案類似的挑戰,儘管技術細節是獨特的。就像 pip install pyarrow
應該在任何地方都能正常運作一樣,R 中的 install.packages("arrow")
也應該如此,我們為此投入了大量精力。由於 R 套件依賴於一個正在積極開發的 C++ 函式庫,這並非易事,尤其是對於 Linux 上 C++ 編譯器和標準函式庫的所有組合而言。
在去年最初的 CRAN 版本 0.14.1 中,只有 Windows 和 macOS 二進位套件可以開箱即用。對於 Linux,您必須先單獨安裝 C++ 函式庫,然後再安裝 R 套件。雖然 Python wheel 檔案即使在 Linux 上也包含二進位函式庫,但 CRAN 僅託管原始碼套件,這些套件必須在安裝時在使用者機器上編譯。這導致 Linux 使用者的體驗不太理想。
從 0.16 版本開始,Linux 上的原始碼套件安裝會自動處理其 C++ 依賴項。預設情況下,套件會執行一個 捆綁腳本,該腳本下載並建置 Arrow C++ 函式庫,除了 R 需要的系統依賴項之外,沒有其他系統依賴項。在許多常見的 Linux 發行版和版本上,可以透過設定環境變數來下載預先建置的靜態 C++ 函式庫以包含在套件中,從而加快速度。
為了配合這些改進並確保它們在各種平台上都能成功,我們在持續整合系統中新增了 廣泛的 每晚建置。這些也易於擴展,我們只需要一個包含 R 的 Docker 映像檔,就可以將新環境插入到我們的常規每晚測試中。
從那時起,我們一直在繼續改進安裝體驗,並尋找減少建置時間和套件大小的方法。上述 C++ 函式庫的改進對 R 套件有所幫助,因為大多數 R 套件的安裝都會建置或以其他方式包含 C++ 函式庫。在 R 套件本身中,我們一直在尋找只包含所需內容而不包含其他內容的方法。這些努力已帶來更小的下載量和安裝套件大小。從 0.17.1 到 1.0.0,macOS 和 Windows CRAN 二進位檔案的已安裝函式庫大小下降了 10%,而 Linux 的預先建置靜態 C++ 函式庫與 0.16.0 相比縮小了 33%,儘管新增了許多新功能。
C 介面
最後,我們觀察到,有些專案可能希望產生或使用 Arrow 格式的子集,並且不想承擔任何額外的程式碼依賴項。在某些情況下,兩個函式庫需要共享記憶體中的 Arrow 資料結構,但無法依賴通用的 Arrow 函式庫,例如參考 C++ 實作。為了應對這些使用案例,我們設計了 C 介面,以提供一種輕量級的方式,在 C 層級交換 Arrow 資料,而無需任何記憶體複製。
當使用 C 介面時,開發人員會填充簡單的 C 資料結構,其中包含有關 Arrow 資料結構的綱要(資料類型)資訊以及構成資料的記憶體片段的位址。這使得函式庫可以在記憶體中輕鬆地組合在一起,而無需任何共享程式碼(除了 C 介面結構定義)。大多數程式語言都具有操作 C 結構的能力,因此即使無需編寫或編譯 C 程式碼也可以使用此介面。我們使用 C 介面透過 reticulate
在記憶體中在 Python 和 R 之間傳輸資料結構。
Arrow C 介面的一個令人興奮的使用案例是將 Arrow 匯入和匯出功能新增到通常包含 C API 的資料庫驅動程式函式庫中。
展望
隨著專案的發展,我們將繼續努力使建置過程盡可能快速和可靠。如果您看到我們可以進一步改進的方法,或者如果您遇到問題,請在我們的郵件列表中提出,或回報問題。