Swift Digest
Blog | Swift.org Blog

UTF-8 文字列

UTF-8 String

このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら

この記事の要点

背景: なぜ UTF-8 なのか

Swift 4.2 までは、String の内部ストレージのエンコーディングは 2 種類ありました。Unicode を多く含むテキスト向けの UTF-16 と、内容がすべて ASCII のとき専用に使われる ASCII ストレージです。Swift 5 では、この 2 つが ASCII と Unicode-rich text の両方をまかなう単一の UTF-8 ストレージに置き換えられました。

UTF-8 は、現代的なコンピューティング環境と効率よく連携するために不可欠です。

たとえばソースコードはほとんどのコンテンツと同様 UTF-8 で表現されるため、SourceKit はソースコード上の位置を UTF-8 バッファ内のオフセットとして扱います。Swift 4.2 で UTF-8 ベースのサービスのクライアントを効率的に書くには、UTF-8 オフセットと UTF-16 インデックスの間の双方向の対応表を保持する必要がありました。UTF-8 のコンテンツから String を作るだけでも UTF-16 への transcoding が発生し、これはコストの高い処理です。前述の SwiftNIO の高速化は、この transcoding をスキップできるようになったことによるものです。

UTF-16 は、すべての scalar が 16 ビットに収まると考えられていた初期の Unicode 向けに設計された環境で使われています。しかし 16 ビットでは足りないことが判明し、現在の Unicode は 21 ビットの scalar を使います。Swift 5 は Objective-C などの UTF-16 ベースのシステムとの効率的な相互運用も提供しますが、String にとって最も効率的な表現は UTF-8 です。

エンコーディングごとの違い

メモリ効率の面では、ASCII 部分について UTF-8 は UTF-16 の半分のメモリで済みます。一方、後半の BMP(Basic Multilingual Plane)の scalar では UTF-8 が UTF-16 より 50% 多くメモリを使います。ただし性能が重要な文字列処理は ASCII を多く含むテキストを扱うことが多く、これは UTF-8 に有利です。中国語の散文(後半 BMP の scalar)でほぼ占められる Web サイトでさえ、HTML タグに ASCII を使うため UTF-8 でエンコードしたほうが小さくなります。識別子・ログメッセージ・URL・各種テキストフォーマットといった「プログラマ向け」の文字列が文字列利用の大半を占めることを考えると、この傾向は重要です。

Swift 4.2 の専用 ASCII 表現はすべて ASCII のコンテンツを効率よく扱えましたが、プログラマ向けの文字列でも Unicode を含む記号などの非 ASCII 文字が混じることは増えています。Swift 4.2 のモデルでは、たった 1 つの非 ASCII scalar があるだけでコンテンツ全体が UTF-16 ストレージに追いやられていました。

また Swift 5 は、Rust と同様、エンコーディングの検証を 生成時に一度だけ 行います。検証のコストはそのタイミングで払うのが最も効率的だからです(Swift にゼロコピーで lazy-bridge される NSString は UTF-16 を使い不正な内容を含み得るため、読み出し時に遅延的に検証されます)。

即時のメリット

C との相互運用

ゼロ終端された UTF-8 文字列は C 文字列と互換性があります。ストレージでゼロ終端を維持しているため、native string はオーバーヘッドなしに C と相互運用できます。myString.withCString { … } のようなコードは、クロージャに C 互換の文字列を渡すためのメモリ確保・transcoding・解放が不要になり、連続ストレージを持つ文字列は内部ポインタをそのまま渡せます(small string はテンポラリなスタック領域へコピーされます)。なお、lazy-bridge された NSString については従来どおり別途の確保/解放と transcoding が必要です。

ストレージ表現の統一

Swift 5 では native のストレージ表現が 2 つから 1 つになりました。これにより、コードサイズやコンパイル時間のコストを抑えつつ、より踏み込んだ解析と積極的な最適化が可能になります。

たとえばインライン化は実行時性能を改善し得る最適化ですが、Swift 4.2 ではほとんどの文字列メソッドがストレージ表現ごとに 2 系統の実装を持っていました。どの表現の文字列であっても、インライン化されたコードの一部はそもそも実行されず、これがインライン化のコストを高め、利点を損なっていました。インライン化の最大の利点は呼び出し箇所ごとの追加解析・最適化から得られますが、2 系統の表現ではこれが格段に難しくなります。統一されたストレージ表現は、インライン化とそれに続く最適化にずっと適しています。

Unicode を含む small string

Swift 4.2 は 64 ビットプラットフォーム向けに、15 個までの ASCII コードユニットからなる文字列をメモリ確保なしで扱える small string 表現を導入していました。しかし 4.2 のモデルでは、これを非 ASCII に拡張するには別のエンコーディングや small string 表現を追加するしかなく、前述の欠点を抱えることになります。

Swift 5 は UTF-8 に切り替わったため、small string は 15 個までの UTF-8 コードユニットの文字列を大きな欠点なしに扱えるようになりました。"smol 🐶! 😍" のような大切な文字列も、実際にコンパクトに保持できます。この設計は 32 ビットプラットフォームにも恩恵があり、Swift 4.2 では small string がなかったのに対し、Swift 5 では 10 個までの UTF-8 コードユニットの small string をサポートします。

既存コードへの影響

コードを変える必要はあるか

大多数の開発者にとって、変更は不要です。

性能上の理由で UTF16View に降りていた場合は、Swift 5 では多くの操作が高速化しているため、ベンチマークを取り直してみるとよいでしょう。それでも低レベルな処理が必要なら、native string にとって最も高速なビューは UTF8View です。

性能が重要なコードでは、String.UTF8View が native string に対して SE-0237 を実装しており、myString.utf8.withContiguousStorageIfAvailable { ... } でメモリ上の連続した UTF-8 バイト列に対してクロージャを実行できます。SE-0247 はこれを土台に、さらに便利な API を提供します。

String.Index.encodedOffset の利用は避ける

SE-0241String.Index.encodedOffset を非推奨にしました。これは誤用が広まっており、Swift 5 ではその問題が表面化しやすかったためです。SE-0241 は、安全でより明示的なインデックス対応づけの代替手段を提供します。

Objective-C との相互運用

String は常に Objective-C API との効率的な相互運用を提供してきましたが、Swift 5 でもそれは変わりません。String のバッキングストレージクラスは NSString のサブクラスであり、コストなしで Objective-C にブリッジされます。新しい UTF-8 バッキングにより、Objective-C API を通じてコンテンツへ直接アクセスできる場面が増え、ブリッジされた String を扱う際の大幅な高速化につながっています。

ただし UTF-8 への切り替えは課題ももたらします。Objective-C API は UTF-16 のインデックスと長さで表現されることが多く、任意の UTF-8 インデックスから UTF-16 インデックスへの変換は本来であれば線形時間のスキャンになります。これではブリッジされた文字列の性能コストとして受け入れられません。

そこで native string は、要求されたときに限り、breadcrumb(パンくず) 戦略によって UTF-8 と UTF-16 のインデックス間を amortized(償却)定数時間で相互変換します。UTF-16 を O(1) でアクセスする API が大きな文字列に対して初めて使われたとき、一度だけ全体をスキャンして一定間隔ごとに breadcrumb を残し、以降の呼び出しに amortized O(1) で答えられるようにします。breadcrumb の粒度を調整することで速度とサイズのバランスを取れます。なお ASCII は UTF-16 のエンコーディングのサブセットであり、すべて ASCII の文字列では UTF-8 オフセットと UTF-16 オフセットが一致するため、breadcrumb を作らずそのまま答えを返します。

まとめ

Swift 5 の String は、ABI 安定化のタイミングに合わせて内部エンコーディングを UTF-8 へ統一しました。利用者のコードはほとんど変更不要のまま、C 連携・最適化・small string・Objective-C 連携といった面で即時のメリットが得られ、さらに将来の高性能な文字列処理 API(SE-0247 など)への土台にもなっています。性能が重要なコードでは UTF8View と関連 API を活用でき、String.Index.encodedOffset の利用だけは見直しが必要です。

関連リンク