Simdutf Can Now Be Used Without libc++ or libc++abi

Mitchell Hashimoto

simdutfがlibc++やlibc++abiなしで使えるようになった

原文は Mitchell Hashimoto により に公開されました。 このブログを購読する

この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つの側面があります。

まず、libc++std::vectorstd::stringなどを提供するC++標準ライブラリです。<vector>はもちろん、<cstring>のようなCライブラリのC++向けラッパーであってもインクルードすれば、libc++に依存することになります。

より厄介なのがlibc++abiで、例外処理や仮想関数テーブル、RTTIなどを含むC++ ABIを提供します。これらを必要とする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++標準ライブラリの型への単純なインクルードや最小限のエイリアスであり、ランタイムオーバーヘッドはありません。

NO_LIBCXXモードでは、stl_compat.hがsimdutfが使用するC++型の独自実装を提供しますが、それはsimdutfが必要とする範囲と互換性を保つのに必要最小限のものです。たとえば、stl_compat.hstd::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への依存がソースレベルでのわかりやすいインクルードとして現れないことでした。一見普通の言語機能に対して、コンパイラがひそかに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_throwtypeinfo__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に依存しなくなりました。

脚注

  1. libc++はC++標準ライブラリ(例: std::vectorstd::stringなど)であり、libc++abiはC++ ABIライブラリ(例: 例外処理、RTTIなど)です。

  2. なお、SIMDを無効にした場合、libghostty-vtは従来から依存関係が一切なく、libcすら必要としません。完全にフリースタンディングなライブラリです。

この記事は「muse-spark-1.2-contributor」を使用して翻訳されました。

コメント