不透明ポインタ

不透明ポインタ型

従来、LLVM IRのポインタ型には、ポインタが指す型が含まれていました。例えば、i32*はメモリ内のi32を指すポインタです。しかし、ポインタが指す型のセマンティクスが不足していることや、ポインタが指す型を持つことによる様々な問題のため、ポインタからポインタが指す型を削除したいという要望があります。

不透明ポインタ型プロジェクトは、LLVM内のポインタが指す型を含むすべてのポインタ型を不透明ポインタ型に置き換えることを目指しています。新しいポインタ型はテキストとしてptrで表現されます。

一部の命令では、ポインタが指すメモリをどのような型として扱うべきかを知る必要があります。例えば、ロード命令はメモリから読み込むバイト数と、結果の値をどのような型として扱うべきかを知る必要があります。このような場合、命令自体に型引数が含まれます。例えば、旧バージョンのLLVMのロード命令

load i64* %p

は、

load i64, ptr %p

アドレス空間は、ローワーリングにおいて区別が必要な異なる種類のポインタ(例:データポインタと関数ポインタは一部のアーキテクチャでサイズが異なる)を区別するために依然として使用されます。不透明ポインタは、アドレス空間やローワーリングに関連するものを変更しません。詳細については、DataLayoutを参照してください。デフォルト以外のアドレス空間にある不透明ポインタはptr addrspace(N)のように記述されます。

これは2015年に提案されました。

明示的なポインタが指す型に関する問題

LLVM IRのポインタは、ポインタが指す型が異なるポインタ間で相互に変換できます。ポインタが指す型は必ずしもメモリの実際の基礎となる型を表しているわけではありません。つまり、ポインタが指す型は実際にはセマンティクスを持ちません。

歴史的にLLVMはCの何らかの型安全なサブセットでした。ポインタが指す型を持つことで、Clangフロントエンドがそのフロントエンドの値/操作を対応するLLVM IRと一致させるための追加のチェック層が提供されました。しかし、C++などの他の言語がLLVMを採用するにつれて、コミュニティは、ポインタが指す型はLLVM開発の妨げになることが多く、一部のフロントエンドでの追加の型チェックはそれだけの価値がないことに気づきました。

LLVMの型システムはもともと高レベルの最適化をサポートするために設計されました。しかし、長年のLLVM実装経験から、ポインタが指す型システムの設計は最適化を効果的にサポートしていないことが示されました。SROA、GVN、AAなどのメモリ最適化アルゴリズムは、一般的にLLVMの構造体型を調べ、基礎となるメモリオフセットについて推論する必要があります。コミュニティは、ポインタが指す型はLLVM開発を助けるのではなく、妨げになっていることに気づきました。当初提案された高レベルの最適化の一部は、高レベルの言語情報をSSA値を介して直接表現するという限界のためにTBAAに進化しました。

ポインタが指す型は、IR検証ツールが型に関する単純なバグを検出するためにフロントエンドにいくらかの価値を提供します。しかし、フロントエンドは、必要な可能性のあるすべての場所にビットキャストを挿入するという複雑さも処理する必要があります。コミュニティのコンセンサスは、ポインタが指す型のデメリットがメリットを上回り、削除する必要があるということです。

多くの操作は、基礎となる型を実際には気にしません。これらの操作は、通常はイントリンシックであり、通常は任意のポインタ型i8*とサイズを時々取ります。これにより、IR内で異なるポインタが指す型との間で冗長なノーオペレーションのビットキャストが大量に発生します。

ノーオペレーションのビットキャストは、メモリ/ディスク容量を消費し、コンパイル時間もかかります。しかし、おそらく最大の課題は、ビットキャストを処理するために必要なコードの複雑さです。ポインタのdef-useチェーンを遡って調べるとき、ビットキャストによって隠された真の基礎となるポインタを見つけるためにValue::stripPointerCasts()を呼び出すことを忘れることが容易です。そして、def-useチェーンを下って調べるとき、パスはビットキャストを繰り返し処理する必要があります。ノーオペレーションのポインタビットキャストを削除すると、最適化の失敗カテゴリが防止され、LLVMパスの記述が少し容易になります。

ノーオペレーションのポインタビットキャストが少なくなると、アドレス空間に関する不正確なビットキャストの可能性も減少します。アドレス空間を非常に気にするバックエンドを保守している人々は、Clangのようなフロントエンドがしばしばポインタを不正確にビットキャストし、アドレス空間情報を失うと不満を述べています。

LLVMで以前に起こった類似の移行は、整数の符号付きです。現在、符号付き整数型と符号なし整数型の間には区別がありません。むしろ、各整数演算(例:加算)には、整数をどのように扱うかを知らせるフラグが含まれています。以前は、LLVM IRは符号付き整数型と符号なし整数型を区別しており、同様のノーオペレーションキャストの問題が発生していました。符号付きを型から命令に移行することは、LLVMの作業を容易にするためにLLVMのタイムラインの初期に行われました。

不透明ポインタモード

移行フェーズの間、LLVMは2つのモードで使用できます。型付きポインタモードでは、すべてのポインタ型にポインタが指す型があり、不透明ポインタは使用できません。不透明ポインタモード(デフォルト)では、すべてのポインタは不透明です。-opaque-pointers=0optなどのLLVMツールで使用するか、clangで-Xclang -no-opaque-pointersを使用することで、不透明ポインタモードを無効にできます。さらに、i8*スタイルの型付きポインタを明示的に記述しているIRとビットコードファイルでは、不透明ポインタモードは自動的に無効になります。

不透明ポインタモードでは、IR、ビットコードで使用されるすべての型付きポインタ、またはPointerType::get()や同様のAPIを使用して作成されたすべての型付きポインタは、自動的に不透明ポインタに変換されます。これにより、移行が簡素化され、不透明ポインタを使用して既存のIRをテストできます。

define i8* @test(i8* %p) {
  %p2 = getelementptr i8, i8* %p, i64 1
  ret i8* %p2
}

; Is automatically converted into the following if -opaque-pointers
; is enabled:

define ptr @test(ptr %p) {
  %p2 = getelementptr i8, ptr %p, i64 1
  ret ptr %p2
}

移行手順

不透明ポインタをサポートするために、2種類の変更が必要になる傾向があります。1つは、PointerType::getElementType()Type::getPointerElementType()へのすべての呼び出しを削除することです。

LLVMミドルエンドとバックエンドでは、これは通常、関連する操作の型を検査することによって実現されます。例えば、メモリアクセスに関連する分析と最適化は、ポインタ型をクエリする代わりに、ロード命令とストア命令にエンコードされている型を使用する必要があります。

ポインタ要素型へのアクセスを回避する一般的な方法を次に示します。

  • ロードの場合はgetType()を使用します。

  • ストアの場合はgetValueOperand()->getType()を使用します。

  • getLoadStoreType()を使用して、上記の両方を1つの呼び出しで処理します。

  • getelementptr命令の場合はgetSourceElementType()を使用します。

  • 呼び出しの場合はgetFunctionType()を使用します。

  • allocaの場合はgetAllocatedType()を使用します。

  • グローバル変数の場合はgetValueType()を使用します。

  • 整合性アサーションの場合はPointerType::isOpaqueOrPointeeTypeEquals()を使用します。

  • 異なるアドレス空間でポインタ型を作成するにはPointerType::getWithSamePointeeType()を使用します。

  • 2つのポインタが同じ要素型を持っているかどうかを確認するにはPointerType::hasSameElementTypeAs()を使用します。

  • 型付きポインタと不透明ポインタの両方を受け入れる方法でコードを作成することをお勧めしますが、Type::isOpaquePointerTy()PointerType::isOpaque()を使用して、不透明ポインタを特別に処理できます。PointerType::getNonOpaquePointerElementType()は、不透明ポインタが明示的に除外されているコードパスでマーカーとして使用できます。

  • byval引数の型を取得するにはgetParamByValType()を使用します。byref、sret、inalloca、preallocatedなど、要素型を知る必要があるその他のABIに影響する属性にも同様のメソッドが存在します。

  • 一部のイントリンシックにはelementtype属性が必要であり、これはgetParamElementType()を使用して取得できます。この属性は、イントリンシックが自然に必要な要素型をエンコードしていない場合に必要です。これはインラインアセンブリにも使用されます。

上記のメソッドの一部は、型付きポインタと不透明ポインタの両方を同時にサポートするためだけに存在し、移行が完了したら削除されます。例えば、isOpaqueOrPointeeTypeEquals()は、すべてのポインタが不透明になったら意味がなくなります。

ポインタ要素型の直接使用はコードですぐに明らかになりますが、不透明ポインタが対処する必要があるより微妙な問題があります。多くのコードは、ポインタの等価性は、使用されるロード/ストア型またはGEPソース要素型も同一であることを意味すると仮定しています。型付きポインタと不透明ポインタの次の例を考えてみてください。

define i32 @test(i32* %p) {
  store i32 0, i32* %p
  %bc = bitcast i32* %p to i64*
  %v = load i64, i64* %bc
  ret i64 %v
}

define i32 @test(ptr %p) {
  store i32 0, ptr %p
  %v = load i64, ptr %p
  ret i64 %v
}

不透明ポインタがない場合、ロードとストアのポインタオペランドが同じであるというチェックは、アクセスされる型も同一であることを保証します。異なる型を使用するにはビットキャストが必要であり、これによりポインタオペランドが区別されます。

不透明ポインタを使用すると、ビットキャストが存在せず、このチェックはもはや十分ではありません。上記の例では、不正確な型のストアからロードへのフォワーディングが発生する可能性があります。このような仮定をしているコードは、アクセスされる型を明示的にチェックするように調整する必要があります。LI->getType() == SI->getValueOperand()->getType()

フロントエンド

フロントエンドは、ローワーリングに必要な限り、LLVMとは独立してポインタが指す型を追跡するように調整する必要があります。例えば、clangは現在、Address構造体でポインタが指す型を追跡しています。

FFIインターフェースを介してC APIを使用するフロントエンドは、多くのC API関数が非推奨となっており、不透明ポインタへの移行の一部として削除されることに注意する必要があります。

LLVMBuildLoad -> LLVMBuildLoad2
LLVMBuildCall -> LLVMBuildCall2
LLVMBuildInvoke -> LLVMBuildInvoke2
LLVMBuildGEP -> LLVMBuildGEP2
LLVMBuildInBoundsGEP -> LLVMBuildInBoundsGEP2
LLVMBuildStructGEP -> LLVMBuildStructGEP2
LLVMBuildPtrDiff -> LLVMBuildPtrDiff2
LLVMConstGEP -> LLVMConstGEP2
LLVMConstInBoundsGEP -> LLVMConstInBoundsGEP2
LLVMAddAlias -> LLVMAddAlias2

さらに、ポインタ型に対してLLVMGetElementType()を呼び出すことはできなくなります。

LLVMContext::setOpaquePointersを使用して、不透明ポインタを使用するかどうかを制御できます(デフォルトをオーバーライドする場合)。

不透明ポインタの一時的な無効化

LLVM 15では、不透明ポインタがデフォルトで有効になっていますが、いくつかのオプトインフラグを使用して型付きポインタを使用することも可能です。

clangドライバインターフェースを使用するユーザーは、-DCLANG_ENABLE_OPAQUE_POINTERS=OFF CMakeオプションを使用するか、-Xclang -no-opaque-pointersを単一のclang呼び出しに渡すことで、古いデフォルトを一時的に復元できます。

clang cc1インターフェースを使用するユーザーは、-no-opaque-pointersを渡すことができます。CLANG_ENABLE_OPAQUE_POINTERS CMakeオプションはcc1インターフェースには影響しません。

LTOでの使用は、-Wl,-plugin-opt=no-opaque-pointersをclangドライバに渡すことで無効にできます。

LLVMをライブラリとして使用するユーザーは、LLVMContextsetOpaquePointers(false)を呼び出すことで、不透明ポインタを無効にできます。

optなどのLLVMツールを使用するユーザーは、-opaque-pointers=0を渡すことで、不透明ポインタを無効にできます。

バージョンサポート

LLVM 14: 不透明ポインタへの移行に必要なすべてのAPIをサポートし、互換性のないAPIを非推奨/削除しています。ただし、最適化パイプラインでの不透明ポインタの使用は完全にサポートされていません。このリリースを使用してツリー外のコードを不透明ポインタと互換性を持たせることができますが、本番環境では不透明ポインタを有効にしないでください

LLVM 15: 不透明ポインタがデフォルトで有効になっています。型付きポインタはまだサポートされています。

LLVM 16: 不透明ポインタがデフォルトで有効になっています。型付きポインタはベストエフォートベースでのみサポートされており、テストされていません。

LLVM 17: 不透明ポインタのみサポートされています。型付きポインタはサポートされていません。

移行状況

2023年7月現在

型付きポインタはmainブランチではサポートされていません

以下の型付きポインタ機能は削除されました。

  • CLANG_ENABLE_OPAQUE_POINTERS CMakeフラグはもはやサポートされていません。

  • -no-opaque-pointers cc1 clangフラグはもはやサポートされていません。

  • -opaque-pointers optフラグはもはやサポートされていません。

  • -plugin-opt=no-opaque-pointers LTOフラグはもはやサポートされていません。

  • 不透明ポインタをサポートしていないC API(LLVMBuildLoadなど)はもはやサポートされていません。

以下の型付きポインタ機能はまだ削除される予定です。

  • 不透明ポインタではもはや関連性のなくなった様々なAPI。