simdutfがlibc++やlibc++abiなしで使えるようになりました
このPRにより、simdutfがlibc++やlibc++abiなしで利用できるようになりました1。
simdutfはlibghostty-vtにおける最後のlibc++依存でした2。この新しいsimdutfビルドを使うようにGhosttyを更新したことで、依存関係からlibc++とlibc++abiを完全に取り除くことができました。
libc++に依存しないことの利点
libc++に依存しないことで、ライブラリはよりポータブルになり(組み込み、WebAssembly、フリースタンディング環境)、クロスコンパイルが容易になり(ターゲット固有のC++標準ライブラリが不要)、バイナリサイズを削減でき、静的リンクもシンプルになります。
汎用の低水準ライブラリとして、simdutfは可能な限りポータブルで柔軟であるべきです。下流の利用者がlibc++を容易に使えるのであればそれでよいのですが、使えない場合でも、simdutfは本質的にそれを必要としないのですから、利用が妨げられるべきではありません。
libc++とlibc++abiの違い
プログラムからlibc++を取り除くには、2つの側面があります。
1つ目は、libc++というC++標準ライブラリです。std::vectorやstd::stringなどを提供します。<vector>はもちろん、<cstring>のようなCライブラリのC++版ヘッダーをインクルードしただけでもlibc++への依存が発生します。
より厄介なのがlibc++abiで、C++のABIを提供します。例外処理や仮想関数テーブル、RTTIなどが含まれます。これらを必要とするC++機能を使った場合、C++標準ライブラリのヘッダーを1つもインクルードしていなくても、libc++abiに依存することになります。
たとえば、次の最小限のC++プログラムは、関数ローカルなstatic変数を使用しているため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++機能をふんだんに活用したC++ライブラリです。個人的な面識はありませんが、Daniel Lemire氏はC++機能を最大限に活用することを好んでいるように見受けられます。つまり、simdutfは紛れもなくC++のプロジェクトなのです。
私の変更が受け入れられる可能性を少しでも高めるため、プロジェクトが引き続き面倒なくC++機能を使い続けられるようにする必要がありました。
STLの利用
私が採用したアプローチは、すべてのC++標準ライブラリ型を一元管理するstl_compat.hヘッダーを導入することでした。通常のlibc++モードでは、stl_compat.h内のすべてが対応するC++標準ライブラリ型への単純なincludeや最小限のエイリアスであり、ランタイムオーバーヘッドはありません。
NO_LIBCXXモードでは、stl_compat.hがsimdutfで使われるC++型の独自実装を提供しますが、それはsimdutfが必要とする範囲と互換性を保つのに必要最小限のものです。たとえば、stl_compat.hはstd::pairの独自実装を提供します。
その結果、差分全体で必要となった変更の大部分は、次のような見た目になりました。
-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への依存がソースコード上の明確なincludeとしては現れないことです。一見普通の言語機能に対して、コンパイラがひそかにC++ ABIランタイムへの呼び出しを生成するために発生します。これを検出するために、オブジェクトファイルを逆コンパイルして__cxa_guard_acquireのようなシンボルを探すスクリプトを書く必要がありました。
simdutfで最大の問題だったのはランタイムディスパッチ層でした。元のコードは関数ローカルなstatic変数に大きく依存していました。C++では、これらのローカル変数は__cxa_guard_acquireや__cxa_guard_releaseといったスレッドセーフな初期化ヘルパーによって保護されますが、これらはC++ ABIランタイムが提供します。そのため、コード中でlibc++abiに一切触れていなくても、コンパイルされたオブジェクトはそれに依存してしまっていました。修正方法は、NO_LIBCXXモードでは関数ローカルなstaticの代わりに翻訳単位のstatic変数を使うことです。
#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インターフェースのサブクラスとしてモデル化しています。この設計自体は維持できますが、抽象クラスのvtableは呼び出されることのない純粋仮想関数のエントリに対しても__cxa_pure_virtualを参照します。SIMDUTF_NO_LIBCXXモードでは、小さなローカルシムを用意し、実行時に実際にはそこに到達しないようにしました。このシンボルは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を私のフォークにある新しいsimdutfを使うように更新し、ビルドをSIMDUTF_NO_LIBCXXを使うように変更した上で、生成物がlibc++やlibc++abiに依存していないことを検証する独自のテストスイートを追加しました。
GhosttyにはUTF-8デコードの挙動(特に不正な入力)を検証する堅牢なテスト群があります。また、さまざまなシナリオでUTF-8のスループットを計測するベンチマークスイートも内蔵しています。Ghosttyのすべてのテストとベンチマークを実行し、すべてがパスすること、そして予想どおりUTF-8のパフォーマンスに影響がないことを確認しました。
プルリクエスト
何かを動くようにすることと、マージされるようにすることは別物です。
メンテナ自身として、「動く」と「マージできる」の間には大きな隔たりがあることをよく理解しています。他者の成果を検証し、今後も安心して保守できるかを見極める難しさも分かります。大きなPRを開く際に、なぜそれが必要なのかが明確でないことの難しさも知っています。そして、最近のAIによる粗製乱造の負担も承知しています。
そこで、私自身が一流のコントリビューターに期待するのと同じだけの労力を注ぎ、simdutfのメンテナにとってそのような存在になろうと努めました。
まず、差分全体(そう、約3,000行すべて)に目を通しました。そして、もう一度見直しました。差分全体を手作業で3〜4回読み返しました。機能的には問題なくても、自分ならコメントしていただろう点に基づいて何度も修正を加えました。
次に、動機、アプローチ、制限事項、検証内容を説明する詳細なPR説明文を手書きで作成しました。メンテナに詳細を理解してもらうだけでなく、どれだけ細部まで考え抜いたかを伝えたかったからです。
最後に、コード作成にAIの支援を受けたことを明示しました。ただし、すべてを手作業でレビューしたこと、PRの説明文やコメントの執筆にはAIを使っていないこと、そして提案した変更のいずれについても、人間として責任を持って説明し修正できることを明確にしました。
皮肉なことに、差分全体をまとめるのに約2時間かかったのに対し、追加の検証作業とPRの準備には約3時間かかりました。メンテナがプロジェクトに注ぐ労力への敬意から、コードそのものよりも、人と人との境界に多くの時間を費やしたのです。
最終的な状況
simdutfのPRは現在もレビュー中です。初期のフィードバックは好意的で、求められる変更には何でも対応するつもりです。メンテナがマージを望まない可能性もありますが、それはそれで構いません。
もしlibc++やlibc++abiなしでsimdutfを使いたい場合は、当面は私のフォークを利用できます。単一ファイル+ヘッダーに統合するアマルガメーションビルドの手順は従来どおりです。C++をビルドしてヘッダーをインクルードする際に、SIMDUTF_NO_LIBCXXを定義すれば、libc++なし版のライブラリが得られます。
GhosttyのPRはすでにマージされています。そのため、libghostty-vtはSIMDビルドにおいてlibc++やlibc++abiに依存しなくなりました。
脚注
記事をランダムに読む