HN 日本語サマリー

← 一覧へ戻る
プログラミング

コンパイラがUTF-8について意見が分かれるとき

When Compilers Disagree About UTF‑8 (nemanjatrifunovic.substack.com)

34 pointsby rbanffy6 コメント

要約

UTF-8デコーディング関数にASCII高速パスを追加する最適化を行ったところ、Clangでは純粋なASCIIテキストでスループットが3倍、混合テキストで34%向上しましたが、GCCではASCIIテキストに全く変化がなく、混合テキストでは逆にパフォーマンスが悪化しました。最終的に、コードポイントの検証をデコード関数内に組み込むことで、Clangのパフォーマンスを維持しつつ、GCCでも15-20%の改善を達成しました。

全文翻訳

コンパイラがUTF-8について意見が分かれるとき 単純なASCII高速パスがClangのパフォーマンスを3倍にしたがGCCには影響がなかった理由 Nemanja Trifunovic 2026年7月26日 共有 2006年の夏、私はUTF-8文字列を扱うためのオープンソースC++ライブラリの作成を開始しました。ポータブルでSTLと良好に連携することを望んでいましたが、当時は最適化にそれほど力を入れていませんでした。最近、UTF-8デコーディングについて書き始めた際に、ライブラリの内部を再検討する時間を費やしました。私が最適化を決定した関数は、UTF-8エンコードされたコードポイントをデコードするものです。 template <typename octet_iterator> utf_error validate_next(octet_iterator& it, octet_iterator end, utfchar32_t& code_point) { if (it == end) return NOT_ENOUGH_ROOM; // 失敗した場合に戻れるように、itの元の値を保存します // もちろん、ストリームイテレータなどではあまり意味がありません octet_iterator original_it = it; utfchar32_t cp = 0; // リードバイトに基づいてシーケンス長を決定します const int length = utf8::internal::sequence_length(it); // トレイルバイトを取得し、コードポイントを計算します utf_error err = UTF8_OK; switch (length) { case 0: return INVALID_LEAD; case 1: err = utf8::internal::get_sequence_1(it, end, cp); break; case 2: err = utf8::internal::get_sequence_2(it, end, cp); break; case 3: err = utf8::internal::get_sequence_3(it, end, cp); break; case 4: err = utf8::internal::get_sequence_4(it, end, cp); break; } if (err == UTF8_OK) { // デコードに成功しました。次に、セキュリティチェックを行います... if (utf8::internal::is_code_point_valid(cp)) { if (!utf8::internal::is_overlong_sequence(cp, length)){ // 合格!ここで返します。 code_point = cp; ++it; return UTF8_OK; } else err = OVERLONG_SEQUENCE; } else err = INVALID_CODE_POINT; } // 失敗した場合 - イテレータの元の値を復元します it = original_it; return err; } これは非常に単純です。リードバイトの値に基づいてシーケンス長を決定し、その長さに応じて、バイトから適切なビットフィールドを抽出してコードポイントの値を構築します。コードポイントが正常にデコードされた後、さらに2つのチェックを実行します。1つはコードポイントの有効性、もう1つは過長UTF-8シーケンスです。両方のチェックに合格した場合、成功ステータスを返し、イテレータは次のシーケンスに進みます。 最適化の機会は、ASCIIがUTF-8のすべての有効性要件を自明に満たすという事実でした。リードバイトの高位ビットがゼロであることが判明した場合、「ASCII文字」が得られます。これは[U+0000, U+007F]の範囲の値であり、常に有効です。ゼロ拡張するだけでコードポイントが得られます。以下のようになります。 template <typename octet_iterator> utf_error validate_next(octet_iterator& it, octet_iterator end, utfchar32_t& code_point) { if (it == end) return NOT_ENOUGH_ROOM; // 失敗した場合に戻れるように、itの元の値を保存します // もちろん、ストリームイテレータなどではあまり意味がありません octet_iterator original_it = it; utfchar32_t cp = 0; // リードバイトに基づいてシーケンス長を決定します const int length = utf8::internal::sequence_length(it); // トレイルバイトを取得し、コードポイントを計算します utf_error err = UTF8_OK; switch (length) { case 0: return INVALID_LEAD; case 1: err = utf8::internal::get_sequence_1(it, end, cp); // 追加の検証は不要です if (err == UTF8_OK) { code_point = cp; ++it; return UTF8_OK; } else { it = original_it; return err; } break; // 警告を避けるため case 2: err = utf8::internal::get_sequence_2(it, end, cp); break; case 3: err = utf8::internal::get_sequence_3(it, end, cp); break; case 4: err = utf8::internal::get_sequence_4(it, end, cp); break; } if (err == UTF8_OK) { // デコードに成功しました。次に、セキュリティチェックを行います... if (utf8::internal::is_code_point_valid(cp)) { if (!utf8::internal::is_overlong_sequence(cp, length)){ // 合格!ここで返します。 code_point = cp; ++it; return UTF8_OK; } else err = OVERLONG_SEQUENCE; } else err = INVALID_CODE_POINT; } // 失敗した場合 - イテレータの元の値を復元します it = original_it; return err; } ASCII専用文字列の処理における目に見える改善と、ASCII/非ASCII混合文字列への軽微な影響を期待していました。clang 18.1.3でのテストは私の期待を上回りました。純粋なASCIIテキストでは、UTF-8デコーディングのスループットは3倍になり、混合テキストでも改善は顕著で、約34%でした。 実際、この変更をGitHubに提出しましたが、その後gccでテストすることにしました。コンパイラは常に私を驚かせます。今回はASCIIテキストに全く違いがありませんでした!ゼロです! 高度に混合されたテキストでは、スループットは一貫して3〜4%悪化しましたが、それはそれほど驚くべきことではありませんでした。 実際に手を動かして、生成されたアセンブリコードを見てみましょう。g++でコンパイルされた元のバージョンでASCIIコードポイントに対して何が起こるかを見てみましょう。 15e0: ldrb w0, [x22] ; バイトをロード 15e4: tbz w0, #7, 1700 ; ビット7が0(ASCII)なら即時リターン... ... 1700: add x22, x22, #1 ; 1バイト消費 1704: cmp x2, x22 ; バッファ終了をチェック 1708: b.ne 15e0 ; 入力があればループ これは最適化され、高度にインライン化されたビルドであり、最後の2つの命令はvalidate_nextをループで呼び出す関数からのものです。しかし、ASCIIブランチに検証チェックがないことは明らかです。コンパイラはチェックが常にパスすることを理解し、それらを単純に削除できました。私の最適化は何も変えませんでした。ASCIIブランチの生成コードは同じままです。次にclangを見てみましょう。 1870: ldrsb w13, [x20] ; バイトをロード、符号拡張 1874: and w12, w13, #0xff ; w12 = 符号なしバージョン 1878: tbnz w13, #31, 1890 ; 符号ビットがセットされていれば非ASCIIパスにジャンプ 187c: mov w14, wzr ; 継続バイトなし 1880: mov w15, #1 ; シーケンス長 = 1バイト 1884: mov x13, x20 ; x13 = リードバイトへのポインタ 1888: mov w16, w12 ; デコードされたコードポイント = バイト値 188c: b 19ac ; 一般的な有効性チェックへ... ... 19ac: lsr w17, w16, #16 19b0: and w18, w16, #0x1ff800 19b4: cmp w17, #0x10 19b8: ccmp w18, w11, #4, ls 19bc: b.eq 2300 ; 無効なコードポイント 19c0: cmp w16, #0x7f 19c4: b.ls 1860 ; 有効なASCII:イテレータを進める 明らかに、clangはコードポイントの有効性チェックを削除しませんでした。私の最適化により、コードは次のようになります。 15e0: ldrb w0, [x22] 15e4: tbz w0, #7, 16c8 ; ASCII... ; ASCII早期リターン完了 16c8: add x22, x22, #1 16cc: cmp x2, x22 16d0: b.ne 15e0 これはclangでの大幅なパフォーマンス向上の理由を確かに説明しています。 最終的に、両方のコンパイラを満足させる、やや大きな変更を加えました。デコード完了後にコードポイントの有効性や過長シーケンスをチェックする代わりに、get_sequence_*関数を拡張してインラインで検証を実行するようにしました。これにより、clangでのパフォーマンス上の利点はすべて維持され、gccでは混合テキストで15〜20%の改善が見られました(純粋なASCIIでは変化なし)。 教訓:コンパイラはトリッキーですが、最適化に時間を費やすことは報われることがあります。 共有 前 次