simdutf 現在已可在不需要 libc++ 或 libc++abi 的情況下使用
原文由 Mitchell Hashimoto 于 發布,訂閱此部落格
自這個 PR 起,simdutf 已經可以在不需要 libc++ 或 libc++abi 的情況下使用了1。
simdutf 是 libghostty-vt 中最後一個對 libc++ 的依賴2。在將 Ghostty 更新為使用這個新的 simdutf 建置版本後,我們得以將 libc++ 與 libc++abi 從依賴中完全移除。
不依賴 libc++ 的好處
不依賴 libc++ 能讓函式庫更具可攜性(嵌入式、WebAssembly、freestanding 環境)、簡化交叉編譯(不需要針對特定目標的 C++ 標準函式庫)、縮小二進位檔大小,並能簡化靜態連結。
作為一個通用、底層的函式庫,simdutf 應該盡可能具備可攜性與彈性。如果下游使用者能輕鬆使用 libc++,那當然很好!但如果不行,也不該因此被阻擋而無法使用 simdutf,畢竟它本質上並不需要它。
libc++ 與 libc++abi 的差別
要讓一個程式擺脫對 libc++ 的依賴,有兩個部分需要處理。
首先,libc++ 是 C++ 標準函式庫,提供了像是 std::vector、std::string 等功能。如果你引入了 <vector>,甚至是像 <cstring> 這類相容於 libc 的 C++ 標頭檔,就代表你依賴了 libc++。
比較隱蔽的部分是 libc++abi,它提供了 C++ ABI,包含例外處理、虛擬函式表、RTTI 等。如果你使用了任何需要這些機制的 C++ 功能,就算你完全沒有引入任何 C++ 標準函式庫標頭檔,你還是在依賴 libc++abi。
舉例來說,下面這個極簡的 C++ 程式就依賴了 libc++abi,因為它使用了函式內的區域靜態變數,而這需要由 C++ ABI 提供的執行緒安全初始化機制:
struct Implementation {
int version;
};
const Implementation& get_impl() {
static const Implementation impl{1};
return impl;
}
int main() {
return get_impl().version;
}將 simdutf 從 libc++ 中抽離
我們先來談 libc++(而非 ABI)。
simdutf 是一個大量運用最新、最強大 C++ 特性的函式庫,雖然我不認識 Daniel Lemire 本人,但他似乎非常喜歡把 C++ 特性發揮到極致。因此,simdutf 是個道道地地的 C++ 專案。
為了讓我的修改有機會被接受,我必須確保專案仍能繼續使用 C++ 特性,而不會因此變得難以維護。
STL 的使用
我決定採取的做法,是引入一個集中管理所有 C++ 標準函式庫型別的stl_compat.h 標頭檔。在一般的 libc++ 模式下,stl_compat.h 中的所有內容都只是對應 C++ 標準函式庫型別的簡單引入或最小程度的別名,不會帶來任何執行時期的額外開銷。
在 NO_LIBCXX 模式下,stl_compat.h 則會為 simdutf 所用到的 C++ 型別提供自己的實作,但僅止於足以相容於 simdutf 需求的程度。舉例來說,stl_compat.h 提供了自己實作的 std::pair。
因此,整個 diff 中的修改大多看起來像這樣:
-std::pair<const char *, char32_t *>
+internal::pair<const char *, char32_t *>
arm_convert_latin1_to_utf32(const char *buf, size_t len,
char32_t *utf32_output) {ABI 相容性
我的目標是盡可能保留 ABI 相容性。部分的公開 ABI 會暴露像是 std::string 這類的 C++ 型別,在這些情況下,ABI 就必須被打破。除此之外,則完全保留。
鑑於 SIMDUTF_NO_LIBCXX 是一個會對編譯單元帶來變更的新功能,我認為僅在啟用這個旗標時打破 ABI 是可以接受的。在未啟用 NO_LIBCXX 旗標的既有情況下,ABI 會被完整保留,現有使用者可以在不破壞 ABI 的前提下更新 simdutf。
令我驚喜的是,ABI 的破壞非常輕微,僅影響少數幾個診斷用函式(例如取得目前實作名稱的函式)和用於處理其他 C++ 型別的輔助函式(例如文字編碼的 std::string)。既然定義上會使用 SIMDUTF_NO_LIBCXX 的人本來就不在意 libc++,這些 ABI 的變動與其說是缺陷,不如說更像是個特性。
將 simdutf 從 libc++abi 中抽離
這項任務要複雜得多。
主要的問題在於,libc++abi 的依賴通常不會以明顯的原始碼層級引入形式出現。它們會出現,是因為編譯器會為看似平常的語言特性悄悄產生對 C++ ABI 執行時期的呼叫。為了偵測這些依賴,我必須撰寫一個腳本來反編譯目的檔,並搜尋像是 __cxa_guard_acquire 這類的符號。
在 simdutf 中最大的元兇是執行時期分派層。原始碼大量依賴函式內的區域靜態變數。在 C++ 中,這些區域變數會由像是 __cxa_guard_acquire 與 __cxa_guard_release 這類執行緒安全初始化輔助函式來保護,而這些函式是由 C++ ABI 執行時期所提供。因此,即使程式碼中完全沒有提到 libc++abi,編譯後的產物仍然依賴它。在 NO_LIBCXX 模式下的修正方式,是改用轉譯單元層級的靜態變數,來取代函式內的區域靜態變數。
#if SIMDUTF_IMPLEMENTATION_ICELAKE
#ifdef SIMDUTF_NO_LIBCXX
static const icelake::implementation icelake_singleton{};
#endif
static const icelake::implementation *get_icelake_singleton() {
#ifdef SIMDUTF_NO_LIBCXX
return &icelake_singleton;
#else
static const icelake::implementation icelake_singleton{};
return &icelake_singleton;
#endif
}
#endif接著,simdutf 將每個後端都建模為抽象 implementation 介面的子類別。這個設計可以保留,但抽象類別的虛擬函式表仍會為那些不可能被呼叫的純虛擬項目參照 __cxa_pure_virtual。在 SIMDUTF_NO_LIBCXX 模式下,我選擇提供一個極小的本地 shim,並確保執行時期永遠不會真的執行到它。我將其標記為 weak,讓該符號在 C++ ABI 存在時可以被覆寫。
#ifdef SIMDUTF_NO_LIBCXX
// The abstract implementation vtable still carries pure-virtual slots even
// though correct dispatch never reaches them in this build mode. Provide the
// narrowest possible ABI shim so stricter no-libcxx objects do not require
// libc++abi just for this unreachable hook. Keep it weak so a toolchain's real
// libc++abi definition wins if one is linked in anyway.
extern "C" SIMDUTF_WEAK [[noreturn]] void __cxa_pure_virtual() noexcept {
__builtin_trap();
}
#endif最後,我撰寫了一個腳本,在使用 -fno-exceptions 與 -fno-rtti 建置時進行稽核,並檢查是否出現像是 __cxa_guard_*、__gxx_personality、__cxa_throw、typeinfo 或 __dynamic_cast 這類符號。這個檢查已被加入 simdutf 的 CI,以確保未來在 NO_LIBCXX 建置中不會意外重新引入對 libc++abi 的依賴。
驗證
內部驗證
simdutf 是一個對正確性與效能都極為關鍵的函式庫,因此我必須確保我的修改不會影響到這兩者。我修改了既有的測試與基準測試套件,使其能在 NO_LIBCXX 與一般模式下都執行,並確保所有測試皆通過、基準測試結果不受影響。
這件事重要之處在於,我提交了必要的修改,以確保 NO_LIBCXX 模式能與現有的測試與基準測試套件相容。這意味著未來對 simdutf 的任何修改,都可以持續對兩種模式進行驗證。
外部驗證:Ghostty
接著,我將 Ghostty 更新為使用來自我 fork 的新版 simdutf,將我們的建置更新為使用 SIMDUTF_NO_LIBCXX,並加入了我們自己的測試套件,以驗證產出的成品沒有對 libc++ 或 libc++abi 的依賴。
Ghostty 有一套完整的測試來驗證我們的 UTF-8 解碼行為(特別是無效輸入的處理)。Ghostty 也有內建的基準測試套件,會在各種情境下測試我們的 UTF-8 處理吞吐量。我執行了所有的 Ghostty 測試與基準測試,並驗證它們全數通過,且如預期般 UTF-8 效能未受影響。
Pull Request
讓某個東西能動,和讓它被合併,是兩回事。
身為維護者,我太清楚「這能動」和「這可以被合併」之間的差距。我知道驗證他人成果、並對未來的維護有信心的挑戰。我知道發起一個大型 PR 卻沒有清楚說明原因的挑戰。我也知道近期 AI 產生的低品質內容所帶來的負擔。
因此,我付出了我會期望一位頂尖貢獻者付出的努力,並試著為 simdutf 維護者成為那樣的人。
首先,我檢視了整個 diff(是的,全部約 3,000 行)。然後又再看了一次。我手動將整個 diff 重讀了三、四遍。即使功能上沒問題,我還是根據那些我自己可能會提出評論的地方,做了多處修改。
接著,我親手撰寫了詳細的 PR 說明,解釋了動機、做法、限制與驗證方式。我想確保維護者不僅了解細節,也能感受到我在細節上投入了多少思考。
最後,我揭露了我確實有使用 AI 來協助撰寫程式碼。但我也明確表示,我已手動審視所有內容,我沒有在撰寫 PR 說明或留言時使用 AI,而且我作為一個人,有能力也樂於為任何提議的修改進行辯護與調整。
諷刺的是,完整的 diff 只花了我約 2 小時就組合完成,但額外的驗證工作與 PR 準備卻花了約 3 小時。我在人與人之間的溝通上花的時間比程式碼本身還多,而出於對維護者投入心力的尊重,本就該如此。
最終狀態
simdutf PR 目前仍在審查中。初步回饋是正面的,我也樂於接受任何要求的修改。維護者也有可能最終不想合併它,那也沒關係。
如果你想在不需要 libc++ 或 libc++abi 的情況下使用 simdutf,期間你可以使用我的 fork。產生單檔整合建置(amalgamated single-file plus header)的指示仍然相同。在建置 C++ 並引入標頭檔時,只要確保定義了 SIMDUTF_NO_LIBCXX,就能取得不依賴 libc++ 的函式庫版本。
Ghostty PR 現已合併。因此,libghostty-vt 在 SIMD 建置中已不再依賴 libc++ 或 libc++abi。
註腳
隨機一篇部落格
留言
登入後參與討論