LLVMにおけるガベージコレクションセーフポイント

ステータス

このドキュメントでは、ガベージコレクションをサポートするためのLLVMの拡張機能について説明します。現在、これらのメカニズムは、完全に再配置するコレクターを搭載した商用Java実装で十分に実証されています。まだバグが残っている可能性のある箇所がいくつかあります。それらは以下に示されています。

これらは、バージョン間で前方または後方互換性の保証が提供されないことを示すために、「実験的」としてリストされています。前方互換性保証が必要な場合は、llvm-devメーリングリストで問題を提起してください。

LLVMは、`gcroot`組み込み関数を使用して保守的なガベージコレクションをサポートする代替メカニズムもサポートしています。 `gcroot`メカニズムは、現在ではほとんど歴史的な関心事ですが、1つの例外があります。シャドウスタックの実装は、多くの言語フロントエンドで正常に使用されており、引き続きサポートされています。

概要とコアコンセプト

デッドオブジェクトを収集するために、ガベージコレクターは、実行中のコード内に含まれるオブジェクトへの参照を識別し、コレクターによっては、それらを更新できる必要があります。コレクターは、コードのすべてのポイントでこの情報を必要とするわけではありません。それは問題をはるかに難しくしますが、「セーフポイント」として知られる実行中の明確に定義されたポイントでのみ必要です。ほとんどのコレクターでは、各固有のポインタ値の少なくとも1つのコピーを追跡すれば十分です。ただし、実行中のコードから直接到達可能なオブジェクトを再配置したいコレクターの場合は、より高い基準が必要です。

もう1つの課題は、コンパイラが、割り当ての外側、あるいは別の割り当ての途中を指す中間結果(「派生ポインタ」)を計算する可能性があることです。この中間値の最終的な使用は、割り当ての境界内にあるアドレスを生成する必要がありますが、このような「外部派生ポインタ」はコレクターから見える場合があります。これを考えると、ガベージコレクターは、アドレスのランタイム値に関連付けられているオブジェクトを示すために安全に依存することはできません。ガベージコレクターがオブジェクトを移動する場合、コンパイラは各ポインタの割り当ての指示へのマッピングを提供する必要があります。

コレクターとコンパイル済みコード間の相互作用を簡素化するために、ほとんどのガベージコレクターは、ロードバリア、ストアバリア、セーフポイントの3つの抽象化で構成されています。

  1. ロードバリアは、マシンロード命令の直後、ロードされた値を使用する前に実行されるコードです。コレクターによっては、すべてのロード、特定のタイプ(元のソース言語)のロードのみ、またはまったくないロードに対して、このようなバリアが必要になる場合があります。

  2. 同様に、ストアバリアは、マシンストア命令の直前、ストアされる値の計算後に実行されるコードフラグメントです。ストアバリアの最も一般的な用途は、世代別ガベージコレクターの「カードテーブル」を更新することです。

  3. セーフポイントは、コンパイル済みコードから見えるポインタ(つまり、現在レジスタまたはスタックにある)が変更される可能性のある場所です。セーフポイントが完了した後、実際のポインタ値は異なる場合がありますが、指し示されている「オブジェクト」(ソース言語から見た場合)は変わりません。

「セーフポイント」という用語は、やや多義的であることに注意してください。マシン状態が解析可能な場所と、アプリケーションスレッドをコレクターがその情報を安全に使用できるポイントにするための調整プロトコルの両方を指します。このドキュメントで使用されている「ステートポイント」という用語は、もっぱら前者を指します。

このドキュメントでは、最後の項目、つまり生成されたコードのセーフポイントのコンパイラサポートに焦点を当てています。セーフポイントをどこに配置するかを決定する外部メカニズムがあると仮定します。私たちの観点からは、すべてのセーフポイントは関数呼び出しになります。コンパイル済みコードの値から直接到達可能なオブジェクトの再配置をサポートするために、コレクターは以下を実行できる必要があります。

  1. セーフポイントでのポインタのすべてのコピー(コンパイラ自体によって導入されたコピーを含む)を識別する

  2. 各ポインタがどのオブジェクトに関連しているかを識別する

  3. 必要に応じて、それらのコピーをそれぞれ更新する

このドキュメントでは、LLVMベースのコンパイラが言語ランタイム/コレクターにこの情報を提供し、必要に応じてすべてのポインタを読み取って更新できるようにするメカニズムについて説明します。

抽象マシンモデル

高レベルでは、LLVMは、ガベージコレクションされたオブジェクトへの参照を表すのに適した非整数ポインタ型で実際のターゲットを拡張する抽象マシンへのコンパイルをサポートするように拡張されています。特に、このような非整数ポインタ型は、整数表現への定義されたマッピングを持っていません。このセマンティックな癖により、ランタイムはプログラムの各ポイントの整数マッピングを選択できるため、オブジェクトの再配置が目に見える影響を与えることなく行えます。.

この高レベルの抽象マシンモデルは、ほとんどのオプティマイザで使用されます。その結果、変換パスは明示的な再配置シーケンスを介して拡張する必要がありません。コード生成を開始する前に、表現を明示的な形式に切り替えます。ローワーリングに選択された正確な場所は実装の詳細です。

抽象マシンモデルの価値のほとんどは、潜在的に再配置可能なオブジェクトをモデル化する必要があるコレクターに由来することに注意してください。再配置しないコレクターのみをサポートするコンパイラの場合は、完全に明示的な形式から始めることを検討してください。

警告:現在、アップストリームで対処されていない非整数ポインタの定義に、既知のセマンティックホールが1つあります。これを回避するには、メモリタイプ(非整数ポインタとその他)が変更されていないことがわかっていない限り、ロードの投機を無効にする必要があります。つまり、非整数ポインタ値が他のタイプとしてロードされるか、その逆の場合にロードを推測することは安全ではありません。実際には、この制限はValueTracking.cppのisSafeToSpeculateによく隔離されています。

明示的な表現

フロントエンドはこの低レベルの明示的な形式を直接生成できますが、そうすると最適化が妨げられる可能性があります。代わりに、再配置コレクターを使用するコンパイラは、上記で説明した抽象マシンモデルをターゲットにすることをお勧めします。

明示的なアプローチの中心は、ガベージコレクターによって実行される可能性のある更新がIRで明示的に見えるようにIRを構築(または書き換え)することです。そのためには、次のことが必要です。

  1. 潜在的に再配置されたポインタごとに新しいSSA値を作成し、セーフポイントの後で元の(再配置されていない)値の使用に到達できないようにする

  2. オプティマイザがステートポイントの後に再配置されていない値の新しい使用を導入できないように、コンパイラに対して不透明な方法で再配置を指定します。これにより、オプティマイザが不健全な最適化を実行するのを防ぎます。

  3. 各ステートポイントのライブポインタ(およびそれらが関連付けられている割り当て)のマッピングを記録する

最も抽象的なレベルでは、セーフポイントの挿入は、呼び出し命令を、呼び出しの元のターゲットを呼び出し、その結果を返し、ガベージコレクションされたオブジェクトへのライブポインタの更新された値を返す複数の戻り値関数の呼び出しに置き換えることと考えることができます。

ガベージコレクションされた値へのすべてのライブポインタを識別し、IRを変換してすべてのライブポインタのベースオブジェクトを与えるポインタを公開し、すべての組み込み関数を正しく挿入するタスクは、このドキュメントの範囲外です。推奨されるアプローチは、以下で説明するユーティリティパスを使用することです。

この抽象関数呼び出しは、まとめて「ステートポイント再配置シーケンス」と呼ばれる一連の組み込み関数呼び出しによって具体的に表されます。

LLVM IRの簡単な呼び出しを考えてみましょう

declare void @foo()
define ptr addrspace(1) @test1(ptr addrspace(1) %obj)
       gc "statepoint-example" {
  call void @foo()
  ret ptr addrspace(1) %obj
}

言語によっては、`foo`の実行中にセーフポイントを許可する必要がある場合があります。その場合、現在のフレームのローカル値をコレクターが更新できるようにする必要があります。そうしないと、最終的に呼び出しから戻ったときに、潜在的に無効な参照にアクセスすることになります。

この例では、SSA値`%obj`を再配置する必要があります。SSA値`%obj`の値を実際に変更することはできないため、セーフポイントの後に`%obj`の潜在的に変更された値を表す新しいSSA値`%obj.relocated`を導入し、後続の使用を適切に更新する必要があります。結果の再配置シーケンスは次のとおりです。

define ptr addrspace(1) @test(ptr addrspace(1) %obj)
       gc "statepoint-example" {
  %safepoint = call token (i64, i32, ptr, i32, i32, ...) @llvm.experimental.gc.statepoint.p0f_isVoidf(i64 0, i32 0, ptr elementtype(void ()) @foo, i32 0, i32 0, i32 0, i32 0) ["gc-live" (ptr addrspace(1) %obj)]
  %obj.relocated = call ptr addrspace(1) @llvm.experimental.gc.relocate.p1(token %safepoint, i32 0, i32 0)
  ret ptr addrspace(1) %obj.relocated
}

理想的には、このシーケンスはM引数、N戻り値関数(Mは再配置される値の数+元の呼び出し引数、Nは元の戻り値+各再配置された値)として表されますが、LLVMはそのような表現を簡単にサポートしていません。

代わりに、statepointイントリンシックは、セーフポイントまたはステートポイントの実際の場所をマークします。 statepointはトークン値(コンパイル時にのみ存在する)を返します。呼び出しの元の戻り値を取得するには、`gc.result`イントリンシックを使用します。各ポインタの再配置を順番に取得するには、適切なインデックスを指定して`gc.relocate`イントリンシックを使用します。 `gc.relocate`と`gc.result`はどちらもstatepointに関連付けられていることに注意してください。この組み合わせは「statepoint再配置シーケンス」を形成し、解析可能な呼び出しまたは「statepoint」全体を表します。

Loweringされると、この例は次のx86アセンブリを生成します

        .globl        test1
        .align        16, 0x90
        pushq %rax
        callq foo
.Ltmp1:
        movq  (%rsp), %rax  # This load is redundant (oops!)
        popq  %rdx
        retq

再配置される可能性のある値はそれぞれスタックにスピルされており、その場所のレコードはスタックマップセクションに記録されています。ガベージコレクタが呼び出し中にこれらのポインタのいずれかを更新する必要がある場合、何を変更すればよいかが正確にわかります。

この例のスタックマップセクションの関連部分は次のとおりです

# This describes the call site
# Stack Maps: callsite 2882400000
        .quad 2882400000
        .long .Ltmp1-test1
        .short        0
# .. 8 entries skipped ..
# This entry describes the spill slot which is directly addressable
# off RSP with offset 0.  Given the value was spilled with a pushq,
# that makes sense.
# Stack Maps:   Loc 8: Direct RSP     [encoding: .byte 2, .byte 8, .short 7, .int 0]
        .byte 2
        .byte 8
        .short        7
        .long 0

この例は、RewriteStatepointsForGCユーティリティパスのテストから取得したものです。そのため、次のコマンドで完全なスタックマップを簡単に調べることができます。

opt -rewrite-statepoints-for-gc test/Transforms/RewriteStatepointsForGC/basics.ll -S | llc -debug-only=stackmaps

再配置を行わないGCの簡略化

前の例の複雑さの一部は、再配置を行わないコレクタには不要です。再配置を行わないコレクタは、どの場所にライブ参照が含まれているかについての情報を必要としますが、明示的な再配置を表す必要はありません。そのため、前述の明示的なLoweringは、すべての`gc.relocate`イントリンシック呼び出しを削除し、元の参照値を使用するように簡略化できます。

再配置を行わないコレクタの前の例の明示的なLoweringは次のとおりです

define void @manual_frame(ptr %a, ptr %b) gc "statepoint-example" {
  %alloca = alloca ptr
  %allocb = alloca ptr
  store ptr %a, ptr %alloca
  store ptr %b, ptr %allocb
  call token (i64, i32, ptr, i32, i32, ...) @llvm.experimental.gc.statepoint.p0(i64 0, i32 0, ptr elementtype(void ()) @func, i32 0, i32 0, i32 0, i32 0) ["gc-live" (ptr %alloca, ptr %allocb)]
  ret void
}

スタック領域の記録

前述の明示的な再配置形式に加えて、statepointインフラストラクチャでは、gcポインタリスト内にallocaをリストすることもできます。 Allocaは、追加の明示的なgcポインタ値と再配置の有無にかかわらずリストできます。

statepointオペランドリストのgc領域にあるallocaは、statepointのスタックマップにスタック領域のアドレスがリストされるようになります。

このメカニズムは、必要に応じて明示的なスピルスロットを記述するために使用できます。セーフポイントの前後で、値がallocaにスピル/フィルされるようにするのは、ジェネレータの責任となります。このような明示的に指定されたスピルスロットに対応するベースポインタを示す方法はないため、使用は、関連するコレクタがポインタ自体からオブジェクトベースを導出できる値に制限されます。

このメカニズムは、コレクタがスタック上の場所から、コレクタが処理する必要がある参照の内部レイアウトを記述するヒープマップにマッピングできる場合に、参照を含むスタックオブジェクトを記述するために使用できます。

警告:現時点では、この代替形式は十分に活用されていません。注意して使用し、いくつかのバグを修正する必要があることを想定することをお勧めします。特に、RewriteStatepointsForGCユーティリティパスは、現在allocaに対して何も実行しません。

ベースポインタと派生ポインタ

「ベースポインタ」は、割り当て(オブジェクト)の開始アドレスを指すポインタです。「派生ポインタ」は、ベースポインタからある量だけオフセットされたポインタです。オブジェクトを再配置する場合、ガベージコレクタは、割り当てに関連付けられた各派生ポインタを新しいアドレスからの同じオフセットに再配置できる必要があります。

「内部派生ポインタ」は、関連付けられている割り当ての境界内に残ります。結果として、割り当ての境界がランタイムシステムにわかっている場合、ベースオブジェクトはランタイムで見つけることができます。

「外部派生ポインタ」は、関連付けられたオブジェクトの境界外にあります。別の割り当てのアドレス範囲内にある場合もあります。結果として、ガベージコレクタがランタイムでどの割り当てに関連付けられているかを判断する方法がなく、コンパイラのサポートが必要です。

`gc.relocate`イントリンシックは、派生ポインタに関連付けられた割り当てを記述するための明示的なオペランドをサポートしています。このオペランドは、多くの場合ベースオペランドと呼ばれますが、厳密に言えばベースポインタである必要はありませんが、関連付けられた割り当ての境界内にある必要があります。一部のコレクタでは、オペランドが単なる内部派生ポインタではなく、実際のベースポインタである必要があります。Lowering中は、ベースがその後で使用されない場合でも、ベースポインタオペランドと派生ポインタオペランドの両方が関連付けられた呼び出しセーフポイントでライブである必要があることに注意してください。

GC遷移

実際的な考慮事項として、多くのガベージコレクションシステムでは、コレクタ対応のコード(「マネージドコード」)がコレクタ対応ではないコード(「アンマネージドコード」)を呼び出すことができます。アンマネージドコードの実行中にコレクタを実行できるようにすることが望ましいため、このような呼び出しもセーフポイントである必要があることが一般的です。さらに、マネージドコードからアンマネージドコードへの遷移の調整には、コレクタに遷移を通知するために呼び出しサイトで追加のコード生成が必要になることがよくあります。これらのニーズをサポートするために、statepointはGC遷移としてマークされ、遷移を実行するために必要なデータ(存在する場合)は、statepointへの追加の引数として提供されます。

多くの場合、関係する関数シンボルに基づいてstatepointがGC遷移であると推測される場合がありますが(たとえば、GC戦略「foo」を持つ関数からGC戦略「bar」を持つ関数への呼び出し)、GC遷移でもある間接呼び出しもサポートする必要があります。この要件は、GC遷移を明示的にマークすることを要求するという決定の背後にある原動力です。

上記のサンプルをもう一度見てみましょう。今回は、`@foo`への呼び出しをGC遷移として扱います。ターゲットによっては、遷移コードは、コレクタに遷移を通知するために、いくつかの追加の状態にアクセスする必要がある場合があります。 TLS変数をアンマネージドコードの呼び出しの前後に書き込む必要がある「hypothetical-gc」という、やや想像力に欠ける名前の仮想GCがあると仮定しましょう。結果の再配置シーケンスは次のとおりです

@flag = thread_local global i32 0, align 4

define i8 addrspace(1)* @test1(i8 addrspace(1) *%obj)
       gc "hypothetical-gc" {

  %0 = call token (i64, i32, void ()*, i32, i32, ...)* @llvm.experimental.gc.statepoint.p0f_isVoidf(i64 0, i32 0, void ()* @foo, i32 0, i32 1, i32* @Flag, i32 0, i8 addrspace(1)* %obj)
  %obj.relocated = call coldcc i8 addrspace(1)* @llvm.experimental.gc.relocate.p1i8(token %0, i32 7, i32 7)
  ret i8 addrspace(1)* %obj.relocated
}

Lowering中は、これにより、次のような命令選択DAGが生成されます

CALLSEQ_START
...
GC_TRANSITION_START (lowered i32 *@Flag), SRCVALUE i32* Flag
STATEPOINT
GC_TRANSITION_END (lowered i32 *@Flag), SRCVALUE i32 *Flag
...
CALLSEQ_END

必要な遷移コードを生成するには、「hypothetical-gc」戦略が特定の関数で使用されている場合に、`GC_TRANSITION_START`ノードと`GC_TRANSITION_END`ノードを適切にLoweringするように、「hypothetical-gc」でサポートされている各ターゲットのバックエンドを変更する必要があります。このようなLoweringがX86に追加されたと仮定すると、生成されるアセンブリは次のようになります

        .globl        test1
        .align        16, 0x90
        pushq %rax
        movl $1, %fs:Flag@TPOFF
        callq foo
        movl $0, %fs:Flag@TPOFF
.Ltmp1:
        movq  (%rsp), %rax  # This load is redundant (oops!)
        popq  %rdx
        retq

上記の設計は完全には実装されていないことに注意してください。特に、戦略固有のLoweringは存在せず、すべてのGC遷移は、呼び出し命令の前後に単一のno-opとして出力されます。これらのno-opは、多くの場合、デッドマシン命令の削除中にバックエンドによって削除されます。

抽象マシンモデルがRewriteStatepointsForGCパスによって明示的なstatepoint再配置モデルにLoweringされる前に、派生ポインタは、それぞれ`gc.get.pointer.base`イントリンシックと`gc.get.pointer.offset`イントリンシックを使用して、ベースポインタとベースポインタからのオフセットを取得できます。これらのイントリンシックは、RewriteStatepointsForGCパスによってインライン化され、このパスの後で使用することはできません。

スタックマップ形式

ランタイムまたはコレクタによって読み取りおよび/または更新が必要になる可能性のある各ポインタ値の場所は、PatchPointドキュメントで指定されているように、生成されたオブジェクトファイルの別のセクションに提供されます。この特別なセクションは、スタックマップ形式に従ってエンコードされます。

一般的な期待は、JITコンパイラがこの形式を解析して破棄することです。これは特にメモリ効率が良いわけではありません。代替形式(たとえば、先行時間コンパイラ用)が必要な場合は、以下の「未解決の作業項目」の議論を参照してください。

各statepointは、次の場所を生成します

  • 呼び出しターゲットの呼び出し規約を記述する定数。この定数は、スタックマップの生成に使用されたLLVMのバージョンで有効な呼び出し規約識別子です。LLVMがこれらの識別子に関して他の場所で提供するものに関して、この定数に追加の互換性保証はありません。

  • statepointイントリンシックに渡されるフラグを記述する定数

  • 後続のdeopt *場所*の数(オペランドではない)を記述する定数。「deopt」バンドルが提供されていない場合は0になります。

  • 可変個数の場所。「deopt」オペランドバンドルにリストされている各deoptパラメータに1つずつ。現時点では、ビット幅が64ビット以下のdeoptパラメータのみがサポートされています。64ビットを超える型の値は、a)呼び出しサイトで値が定数であり、b)定数が64ビット未満で表すことができる場合(元のビット幅へのゼロ拡張を想定)にのみ指定および報告できます。

  • 可変個数の再配置レコード。それぞれが正確に2つの場所からなります。再配置レコードについては、以下で詳しく説明します。

各再配置レコードは、コレクタが1つ以上の派生ポインタを再配置するのに十分な情報を提供します。各レコードは、場所のペアで構成されます。レコードの2番目の要素は、更新が必要なポインタ(複数可)を表します。レコードの最初の要素は、再配置されるポインタが関連付けられているオブジェクトのベースへのポインタを提供します。ポインタは元の割り当ての境界外にある場合がありますが、それでも割り当てとともに再配置する必要があるため、この情報は一般化された派生ポインタを処理するために必要です。さらに

  • ベースポインタは、statepointの後で使用される場合、再配置ペアとして明示的に表示される必要があることが保証されています。

  • IRのステートポイントにあるGCパラメータよりも再配置レコードが少ない場合があります。それぞれの*固有の*ペアは少なくとも1回出現しますが、重複する可能性があります。

  • 各レコード内のロケーションは、ポインタサイズ、またはポインタサイズの倍数のいずれかです。後者の場合、レコードはポインタのシーケンスと対応するベースポインタを記述するものとして解釈する必要があります。ロケーションのサイズがN x sizeof(pointer)の場合、ロケーション内にポインタ1つずつのN個のレコードが含まれます。ペア内の両方のロケーションは同じサイズであると想定できます。

各セクションで使用されるロケーションは、同じ物理ロケーションを記述している場合があることに注意してください。例えば、スタックスロットは、deoptロケーション、GCベースポインタ、およびGC派生ポインタとして出現する可能性があります。

StkMapRecordのLiveOutセクションは、ステートポイントレコードでは空になります。

セーフポイントのセマンティクスと検証

コンパイルされたコードのガベージコレクタに関する基本的な正当性プロパティは、動的なものです。再配置される可能性のあるポインタを含む操作が、それを再配置する可能性のあるセーフポイントの後で*観測可能*にならないような動的なトレースが存在しないことが必要です。この用法における「観測可能」とは、外部のオブザーバーが、操作がセーフポイントの前に実行されることを妨げる方法で、この一連のイベントを観測できることを意味します。

この「観測可能」プロパティが必要な理由を理解するために、再配置されたポインタの元の複製に対して実行されるnull比較を考えてみましょう。制御フローがセーフポイントに従うと仮定すると、null比較がセーフポイントの前または後に行われるかを外部から観察する方法はありません。(元の値はセーフポイントによって変更されないことを忘れないでください。)コンパイラは、どちらのスケジューリングを選択することもできます。

実際に実装されている正当性プロパティは、これよりも少し強力です。再配置される可能性のあるポインタが再配置された*後*に「観測可能」になる*静的パス*が存在しないことを要求します。これは厳密に必要なものよりもわずかに強力ですが(そのため、有効なプログラムの一部を許可しない場合があります)、コンパイルされたコードの正当性に関する推論を大幅に簡素化します。

構造上、このプロパティは、ソースIRで正しく確立されていれば、オプティマイザによって維持されます。これは、設計の重要な不変条件です。

既存のIR検証パスは、それぞれのドキュメントで言及されている組み込み関数のローカル制限のほとんどをチェックするように拡張されています。LLVMの現在の実装では、主要な再配置の不変条件はチェックされていませんが、このような検証器の開発は進行中です。現在のバージョンを試してみたい場合は、llvm-devでお尋ねください。

セーフポイント挿入のためのユーティリティパス

RewriteStatepointsForGC

RewriteStatepointsForGCパスは、関数のIRを変換して、上記の抽象マシンモデルから明示的な再配置のステートポイントモデルに下げます。これを行うために、セーフポイントポーリングを含む可能性のある関数のすべての呼び出しまたは呼び出しを、`gc.statepoint` と、必要なすべての `gc.relocates` を含む完全な再配置シーケンスに置き換えます。

このパスは、`UseRS4GC` フラグが設定されているGCStrategyインスタンスにのみ適用されます。このフラグが設定されている2つの組み込みGC戦略は、「statepoint-example」戦略と「coreclr」戦略です。

例として、次のコードがあるとします

define ptr addrspace(1) @test1(ptr addrspace(1) %obj)
       gc "statepoint-example" {
  call void @foo()
  ret ptr addrspace(1) %obj
}

このパスは、次のIRを生成します

define ptr addrspace(1) @test_rs4gc(ptr addrspace(1) %obj) gc "statepoint-example" {
  %statepoint_token = call token (i64, i32, ptr, i32, i32, ...) @llvm.experimental.gc.statepoint.p0(i64 2882400000, i32 0, ptr elementtype(void ()) @foo, i32 0, i32 0, i32 0, i32 0) [ "gc-live"(ptr addrspace(1) %obj) ]
  %obj.relocated = call coldcc ptr addrspace(1) @llvm.experimental.gc.relocate.p1(token %statepoint_token, i32 0, i32 0) ; (%obj, %obj)
  ret ptr addrspace(1) %obj.relocated
}

上記の例では、ポインタのaddrspace(1)マーカーは、`statepoint-example` GC戦略が参照と非参照を区別するために使用するメカニズムです。これは、GCStrategy::isGCManagedPointerによって制御されます。`statepoint-example` 戦略と `coreclr` 戦略(ステートポイントをサポートする唯一の2つのデフォルト戦略)は両方とも、どのポインタが参照であるかを判断するためにaddrspace(1)を使用しますが、カスタム戦略はこの規則に従う必要はありません。

このパスは、IRを構築するときにliveness、ベースポインタ、または再配置について手動で推論したくない言語フロントエンドによって、ユーティリティ関数として使用できます。現在の実装では、RewriteStatepointsForGCはSSA構築(つまりmem2ref)の後で実行する必要があります。

RewriteStatepointsForGCは、作成されるすべての再配置に適切なベースポインタがリストされていることを保証します。これは、再配置される各ポインタに関連付けられたベースポインタを適切なセーフポイントに伝達するために、必要に応じてコードを複製することによって行います。実装では、次のIR構造がベースポインタを生成すると想定しています。ヒープからのロード、グローバル変数のアドレス、関数引数、関数の戻り値。定数ポインタ(nullなど)もベースポインタであると想定されます。実際には、ターゲットコレクタが任意の内部派生ポインタから関連する割り当てを見つけることができる場合、この制約は内部派生ポインタを生成するように緩和できます。

デフォルトでは、RewriteStatepointsForGCは、新しく構築された`gc.statepoint` に、ステートポイントIDとして`0xABCDEF00` を、パッチ可能なバイト数として`0` を渡します。これらの値は、属性`"statepoint-id"` と `"statepoint-num-patch-bytes"` を使用して、呼び出しサイトごとに設定できます。呼び出しサイトが`"statepoint-id"` 関数属性でマークされ、その値が正の整数(文字列として表される)である場合、その値は新しく構築された`gc.statepoint` のIDとして使用されます。呼び出しサイトが`"statepoint-num-patch-bytes"` 関数属性でマークされ、その値が正の整数である場合、その値は新しく構築された`gc.statepoint` の「パッチバイト数」パラメータとして使用されます。`"statepoint-id"` 属性と `"statepoint-num-patch-bytes"` 属性は、正常に解析できた場合、`gc.statepoint` 呼び出しまたは呼び出しに伝播されません。

実際には、RewriteStatepointsForGCは、ほとんどの最適化がすでに完了した後、パスパイプラインのはるかに後で実行する必要があります。これは、ガベージコレクションサポートを有効にしてコンパイルする場合に、生成されるコードの品質を向上させるのに役立ちます。

RewriteStatepointsForGC組み込み関数 Lowering

明示的な再配置モデルへのLoweringの一環として、RewriteStatepointsForGCは、次の組み込み関数に対してGC固有のLoweringを実行します

  • gc.get.pointer.base

  • gc.get.pointer.offset

  • llvm.memcpy.element.unordered.atomic.*

  • llvm.memmove.element.unordered.atomic.*

memcpy操作とmemmove操作には、GCリーフLoweringとGC解析可能Loweringの2つのLoweringが可能です。呼び出しが明示的に「gc-leaf-function」属性でマークされている場合、呼び出しは「`__llvm_memcpy_element_unordered_atomic_*`」または「`__llvm_memmove_element_unordered_atomic_*`」シンボルへのGCリーフ呼び出しにLoweringされます。このような呼び出しは、セーフポイントを取得できません。そうでない場合、呼び出しをステートポイントでラップすることにより、呼び出しはGC解析可能になります。これにより、コピー操作中にセーフポイントを取得することが可能になります。GC解析可能コピー操作は、セーフポイントを取得する必要がないことに注意してください。たとえば、短いコピー操作は、セーフポイントを取得せずに実行できます。

`llvm.memcpy.element.unordered.atomic.*`'、`llvm.memmove.element.unordered.atomic.*` 組み込み関数へのGC解析可能呼び出しは、「`__llvm_memcpy_element_unordered_atomic_safepoint_*`」、「`__llvm_memmove_element_unordered_atomic_safepoint_*`」シンボルへの呼び出しにそれぞれLoweringされます。これにより、ランタイムはセーフポイントありとなしのコピー操作の実装を提供できます。

GC解析可能Loweringには、呼び出しの引数の調整も含まれます。Memcpy組み込み関数とmemmove組み込み関数は、ソース引数と宛先引数として派生ポインタを取ります。コピー操作がセーフポイントを取得する場合、基になるソースオブジェクトと宛先オブジェクトを再配置する必要がある場合があります。これには、対応するベースポインタがコピー操作で使用可能である必要があります。ベースポインタを使用できるようにするために、RewriteStatepointsForGCは派生ポインタをベースポインタとオフセットのペアに置き換えます。例えば

declare void @__llvm_memcpy_element_unordered_atomic_safepoint_1(
  i8 addrspace(1)*  %dest_base, i64 %dest_offset,
  i8 addrspace(1)*  %src_base, i64 %src_offset,
  i64 %length)

PlaceSafepoints

PlaceSafepointsパスは、実行中のコードがセーフポイント要求を適時にチェックすることを保証するのに十分なセーフポイントポーリングを挿入します。このパスはRewriteStatepointsForGCの前に実行されることが想定されているため、完全な再配置シーケンスは生成されません。

例として、次の入力IRが与えられた場合

define void @test() gc "statepoint-example" {
  call void @foo()
  ret void
}

declare void @do_safepoint()
define void @gc.safepoint_poll() {
  call void @do_safepoint()
  ret void
}

このパスは、次のIRを生成します

define void @test() gc "statepoint-example" {
  call void @do_safepoint()
  call void @foo()
  ret void
}

この場合、(無条件の)エントリセーフポイントポーリングを追加しました。外観にもかかわらず、エントリポーリングは必ずしも冗長ではないことに注意してください。ポーリングが冗長であるためには、`foo` と `test` が相互再帰的ではないことを知る必要があります。実際には、ポーリング定義に何らかの形式の条件分岐を含めることが望ましいでしょう。

現時点では、PlaceSafepointsは、メソッドエントリとループバックエッジの場所にセーフポイントポーリングを挿入できます。必要に応じて、これをリターンポーリングで動作するように拡張するのは簡単です。

PlaceSafepointsには、通常の条件下でポーリングの適時な実行を保証するために必要でない限り、特定のサイトにセーフポイントポーリングを配置することを避けるための多くの最適化が含まれています。PlaceSafepointsは、システムのページングが激しいなど、最悪の条件下でのポーリングの適時な実行を保証しようとはしません。

セーフポイントポーリングアクションの実装は、包含モジュールで`gc.safepoint_poll` という名前の関数を検索することによって指定されます。この関数の本体は、必要な各ポーリングサイトに挿入されます。このメソッド内の呼び出しまたは呼び出しは`gc.statepoints` に変換されますが、再帰的なポーリング挿入は実行されません。

このパスは、セーフポイントでのみガベージコレクションセマンティクスをサポートする必要がある言語フロントエンドに役立ちます。セーフポイントで他の抽象フレーム情報(逆最適化やイントロスペクションなど)が必要な場合は、フロントエンドにセーフポイントポーリングを挿入できます。後者の場合は、llvm-devで提案を求めてください。このようなスキームを実際にうまく機能させるための作業がかなり行われていますが、まだここでは説明していません。

サポートされているアーキテクチャ

ステートポイント生成のサポートには、各バックエンド向けのコードが必要です。現在、Aarch64とX86_64のみがサポートされています。

制限事項と未完成のアイデア

参照と生ポインタの混在

抽象マシンモデルにおいて、ガベージコレクション対象オブジェクトへの管理されていないポインタを許可する言語(つまり、オブジェクトへのポインタをCルーチンに渡す)のサポート。現時点では、これに対処するための最良のアイデアは、参照値と生ポインタ間の接続を隠す組み込み関数または不透明関数を使用することです。問題は、ptrtointまたはinttoptrキャスト(このようなユースケースでは一般的です)があると、抽象モデルから明示的な物理モデルへの低下時に任意の参照のベースポインタを推測するために使用されるルールが破られることです。物理モデルに直接低下するフロントエンドには、ここでは問題がないことに注意してください。

スタック上のオブジェクト

上記のように、明示的な低下は、コレクターがスタックアドレスを指定してヒープマップを見つけることができる場合、スタックに割り当てられたオブジェクトをサポートします。

不足している部分は、a)抽象マシンモデルからの書き換え(RS4GC)との統合、およびb)スタックオブジェクトをオプションで分解して、ヒープマップを必要としないようにするためのサポートです。後者は、一部のコレクターとの統合を容易にするために必要です。

低下の品質と表現のオーバーヘッド

現在のステートポイントの低下はやや貧弱であることが知られています。非常に長期的な展望では、ステートポイントをレジスタアロケータと統合したいと考えています。近い将来、これは起こりそうにありません。ホットステートポイントはほとんど常にインライナーのバグであるため、低下の品質は比較的重要ではないことがわかりました。

ステートポイント表現により、一部の例で大量のIRが生成され、これが予想よりも高いメモリ使用量とコンパイル時間に寄与するという懸念が提起されています。これによる変更を行う予定はすぐにはありませんが、将来的には代替モデルが検討される可能性があります。

例外エッジに沿った再配置

例外パスに沿った再配置は、現在ToTで壊れています。特に、再配置もあるパスで再スローを表す方法が現在ありません。詳細については、このllvm-devの議論を参照してください。

バグと拡張機能

現在認識されているバグと検討中の拡張機能は、サマリーフィールドで[Statepoint]をbugzilla検索することで追跡できます。新しいバグを提出する場合は、関係者が新しく提出されたバグを確認できるように、このタグを使用してください。ほとんどのLLVM機能と同様に、設計に関する議論はDiscourseフォーラムで行われ、パッチはレビューのためにllvm-commitsに送信する必要があります。