
2026/08/01 5:07
早期に終了しないでください:メモリ速度でケースフォールドされたソースコード
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
概要
Rust の
casefold クレイトは、速度性とスケーラビリティを優先することで Unicode ケースフォールディングにおいて卓越したパフォーマンスを発揮します。ASCII テキストではメモリ帯域速度が 45 GiB/s を超える達成を実現しており、これは分岐のない設計を採用し、遅延を引き起こす条件分岐やヒープ割り当てを回避するとともに、コンパクトな 1776 バイトのルックアップテーブルと最適化されたバイト演算を用いて、完全な UTF-8 デコードサイクルを経ることなくデータを直接処理することによって成り立っています。この高速性を維持するために、複雑なマルチ文字フォールド規則やロケール固有の例外(トルコ語の点のない İ など)を意図的に除外しており、その結果としてテキスト表示の整合性を保ちつつケース不感応一致を可能にする効率的な処理を実現しています。このアーキテクチャは、GitHub の Blackbird という 1.8 億を超えるリポジトリをインデックス化する大規模アプリケーションにとって不可欠であり、クレイトを活用することで同様のシステムは、単純な代替案や simd_normalizer といったより遅いベクトライゼーション手法よりもはるかに高速に膨大なデータセットを処理できるようになります。本ライブラリは健全な最短形式 UTF-8 に依存しているため、潜在的な符号化問題のある生バイトの検証には外部チェックが必要となりますが、この設計は様々なハードウェアプラットフォームにおいてスケーラブルに機能します。本文
大規模コード検索エンジン向け高速ケースフォールドアルゴリズム
背景:なぜケースフォールドが必要か?
ユーザーが「café」と検索して、「CAFÉ」を一致させる必要がある場合、**大文字小文字の違いを無視する処理(ケースフォールド)**が必要です。これは以下の場面で利用されます:
- 検索エンジンの照合処理
- 正規表現の
フラグ(?i) - 大文字小文字に不敏感なユーザー名やホスト名の管理
GitHub のコード検索エンジン Blackbird は、インデックス化した 1 億 8,000 万以上のリポジトリ(合計 480TB)を処理するため、極めて単純なケースフォールド処理でも速度の限界が問題となります。
ケースフォールドとは大文字小文字変換ではない
str::to_lowercase とケースフォールドは目的が異なります:
| 比較項目 | 大文字小文字変換 (Lowercasing) | ケースフォールド (Case Folding) |
|---|---|---|
| 目的 | 表示用 | 比較・照合用 |
| 依存性 | ローカライゼーションや文脈に依存 | 文脈やローカライゼーションに依存しない設計 |
| 特徴 | ローカル化される(例:ギリシャ語のΣ) | 対称性を維持(A→B と B→A が常に一致) |
- 実装の制限:
クレイトは、1 対 1 のフォールドのみを実装しています。casefold- 「多文字化するフルフォールド」(例:ß → ss)や「ローカライゼーション特有のフォールド」(例:トルコ語の点付きイ İ)は実装していません。
- ツール間の整合性のため、一般的なツール(ripgrep 等)と同様の制限を設けています。
直感に反する最適化:ブランチと早期中断を削除する
ASCII 文字が绝大多数であるため、**メモリアダプタンス(帯域幅)**速度に達することが最優先です。そのために、「早期中断」や「条件分岐(ブランチ)」を持つコードは却下されます。
ブランチレスな処理の原理
- 早期中断を削除: 非 ASCII バイトを見つけたら即座に中断せず、バッファ全体を一括処理します。これによりコンパイラが最適化できます。
- 条件分岐(ブランチ)を消去:
ステートメントによる分岐を持たせ、代わりに算術計算とビット操作を用います。if
速度向上の成果比較 (Apple M4)
| バージョン | Throughput | ベクトライズ済み? | 備考 |
|---|---|---|---|
| naive(中断あり・ブランチあり) | 3.1 GiB/s | いいえ | ブランチがボトルネック |
| 中断のみ削除 | 2.6 GiB/s | 一部 | ブランチ残るため速度低下 |
| 早期終了の中断を削除 | 7.6 GiB/s | 一部 (命令数 25) | 依然として限界がある |
| ブランチレスなテスト + 書き込み | >45 GiB/s | 完全 (命令数 41) | メモリアダプタンス到達 |
- 教訓: スカラーコードにおいて、ブランチレス化はベクトライズが可能になる場合のみ価値があります。それ自体ではコスト増を招く可能性があります。
- 中間案の限界: ASCII 検出と変換を分離して行う「双パスアプローチ」は、早期終了ブランチによりブロック固定され、8.7 GiB/s までしか上がらず、単一パス(>45 GiB/s)の方が速く有効です。
ヒープ領域の回避(In-Place Fold)
不要な割り当てをゼロに抑えます:
- 入力バッファを値で受け取り: ASCII であれば就地(in-place)でフォールドし、第 2 のバッファは使用しません。
- 動的拡張: フォールドにより文字列が長くなるときのみ、一度きりの事前割り当て(
)を行います。capacity = len + len/2 - マルチバイト文字の扱い: CJK やアラビア語などフォールドしないテキストも、コピーせずそのまま返します。
Unicode を安価に扱う(1,776 バイトのテーブル)
デコードしてハッシュ検索を行う従来の方法では非効率です。Unicode 16.0 の 1,484 つのフォールドマッピングを圧縮し、1,776 バイトという驚くほど小さい構造体で実装しています。
- ページ化ビットマップ: フォールドしない文字は単一ビットテストで拒絶可能。
- ラン記録 (Runs): 隣接するコードポイントの類似性を活用し、検索を高速化。
- バイト空間算術: UTF-8 をデコードせず、バイト単位での加算処理でフォールドを実行。
代替案との比較
| レプレゼンテーション | サイズ (B) | 特性 |
|---|---|---|
Naïve | ~11,600 | デコード処理あり |
| regex-syntax のテーブル | ~70,000 | 大規模 |
Go の | ~7,300 | 比較的大きい |
| Rust HashMap | ~17,000 | ハッシュ計算コスト |
このクレイト () | 1,776 | 桁違いに小さく、デコード不要 |
パフォーマンスベンチマーク (8.8KB 最悪ケースなど)
| ワークロード | simple_fold | simd_normalizer | HashMap |
|---|---|---|---|
| 純粋な ASCII | >45 GiB/s | 1.21 GiB/s | 213 MiB/s |
| 漢字系(フォールドなし) | 2.95 GiB/s | 1.97 GiB/s | 558 MiB/s |
| 記号系(フォールドなし) | 2.96 GiB/s | 1.56 GiB/s | 410 MiB/s |
| 最悪ケース:全フォールド | 869 MiB/s | 922 MiB/s | 334 MiB/s |
※数値はプラットフォーム依存しますが、simple_fold は ASCII 時にメモリアダプタンス速度に達し、非 ASCII でも SIMD フォルダーを上回る性能を発揮します。
まとめ:なぜこのアプローチが有効か
ケースフォールド処理で達成した主な成果は以下の 2 つの直感と逆向きのアイデアによるものです。
- ブランチレス・スキャン: バッファ全体を分岐なしで一括処理し、メモリアダプタンス速度(>45 GiB/s)に到達。
- デコードフリーなバイト空間算術: Unicode キャラクターをデコードせず、バイト単位の加算処理でフォールドを実行。
これにより、一般的な ASCII 処理では不可能なスピードを実現するとともに、レアケースであるフォールドも高効率で行うことを可能にしました。
- クレイト名:
casefold - リポジトリ: GitHub の
クレイト(ソースと設計ノート同梱)casefold