LLVM ブランチウェイトメタデータ

はじめに

ブランチウェイトメタデータは、ブランチが実行される可能性をウェイトとして表します(LLVM ブロック頻度用語を参照)。メタデータは、ターミネータであるInstructionMD_prof種類のMDNodeとして割り当てられます。最初のオペランドは常に、文字列「branch_weights」を持つMDStringノードです。オペランドの数はターミネータの種類によって異なります。

ブランチウェイトは、プロファイリングファイルからフェッチされるか、__builtin_expectおよび__builtin_expect_with_probability命令に基づいて生成される場合があります。

すべてのウェイトは符号なし32ビット値として表され、値が大きいほど実行される可能性が高いことを示します。

サポートされている命令

BranchInst

メタデータは、条件付きブランチにのみ割り当てられます。trueブランチとfalseブランチには、2つの追加のオペランドがあります。オプションで、メタデータが__builtin_expectまたは__builtin_expect_with_probabilityによって追加されたかどうかを、オプションのフィールド!"expected"で追跡します。

!0 = !{
  !"branch_weights",
  [ !"expected", ]
  i32 <TRUE_BRANCH_WEIGHT>,
  i32 <FALSE_BRANCH_WEIGHT>
}

SwitchInst

ブランチウェイトは、すべてのcase(常にcase#0であるdefault caseを含む)に割り当てられます。

!0 = !{
  !"branch_weights",
  [ !"expected", ]
  i32 <DEFAULT_BRANCH_WEIGHT>
  [ , i32 <CASE_BRANCH_WEIGHT> ... ]
}

IndirectBrInst

ブランチウェイトは、すべての宛先に割り当てられます。

!0 = !{
  !"branch_weights",
  [ !"expected", ]
  i32 <LABEL_BRANCH_WEIGHT>
  [ , i32 <LABEL_BRANCH_WEIGHT> ... ]
}

CallInst

呼び出しには、呼び出しの実行回数を含むブランチウェイトメタデータが含まれる場合があります。現在、これはSamplePGOモードでのみ使用され、サンプリングでは正確ではない可能性があるブロック数とエントリ数を増強します。

!0 = !{
  !"branch_weights",
  [ !"expected", ]
  i32 <CALL_BRANCH_WEIGHT>
}

InvokeInst

Invoke命令には、1つまたは2つのウェイトを持つブランチウェイトメタデータを含めることができます。2番目のウェイトはオプションで、アンワインドブランチに対応します。ウェイトが1つだけ設定されている場合、そこには呼び出しの実行回数が含まれており、呼び出し命令の説明にあるように、SamplePGOモードでのみ使用されます。両方のウェイトが指定されている場合、2番目のウェイトにはアンワインドブランチの実行回数が含まれており、最初のウェイトには呼び出しの実行回数からアンワインドブランチの実行回数を引いたものが含まれます。指定された両方のウェイトは、BranchInstの場合と同様にBranchProbabilityを計算するために使用され、SamplePGOでは両方のウェイトの合計が使用されます。

!0 = !{
  !"branch_weights",
  [ !"expected", ]
  i32 <INVOKE_NORMAL_WEIGHT>
  [ , i32 <INVOKE_UNWIND_WEIGHT> ]
}

その他

その他のターミネータ命令には、ブランチウェイトメタデータを含めることはできません。

組み込みexpect命令

__builtin_expect(long exp, long c)命令は、ブランチ予測情報を提供します。戻り値はexpの値です。

これは、条件付きステートメントで特に役立ちます。現在、Clangは2つの条件付きステートメントをサポートしています

ifステートメント

expパラメータは条件です。cパラメータは、予想される比較値です。これが1(true)と等しい場合、条件はtrueになる可能性が高く、それ以外の場合は条件がfalseになる可能性が高くなります。たとえば

if (__builtin_expect(x > 0, 1)) {
  // This block is likely to be taken.
}

switchステートメント

expパラメータは値です。cパラメータは、予想される値です。予想される値がcaseリストに表示されない場合、default caseが実行される可能性が高いと想定されます。

switch (__builtin_expect(x, 5)) {
default: break;
case 0:  // ...
case 3:  // ...
case 5:  // This case is likely to be taken.
}

組み込みexpect.with.probability命令

__builtin_expect_with_probability(long exp, long c, double probability)は、__builtin_expectと同じセマンティクスを持ちますが、呼び出し元はexp == cである確率を提供します。最後の引数probabilityは、定数浮動小数点式である必要があり、[0.0、1.0]の範囲内である必要があります。使用法も__builtin_expectと同様です。たとえば

ifステートメント

期待比較値cが1(true)と等しく、確率値probabilityが0.8に設定されている場合、条件がtrueになる確率は80%であり、falseになる確率は20%であることを意味します。

if (__builtin_expect_with_probability(x > 0, 1, 0.8)) {
  // This block is likely to be taken with probability 80%.
}

switchステートメント

これは、基本的には__builtin_expectswitchステートメントと同じです。expが期待値と等しくなる確率は、3番目の引数probabilityで与えられますが、他の値の確率は残りの確率(1.0 - probability)の平均です。例えば

switch (__builtin_expect_with_probability(x, 5, 0.7)) {
default: break;  // Take this case with probability 10%
case 0:  break;  // Take this case with probability 10%
case 3:  break;  // Take this case with probability 10%
case 5:  break;  // This case is likely to be taken with probability 70%
}

CFGの変更

ブランチウェイトメタデータは、CFGの変更に対して安全ではありません。ターミネータオペランドが変更された場合は、何らかのアクションを実行する必要があります。それ以外の場合、誤ったブランチ予測情報により、最適化の誤りが発生する可能性があります。

関数エントリ数

インタープロシージャル分析と最適化中に異なる関数を比較できるようにするために、MD_profノードを関数定義に割り当てることもできます。最初のオペランドは、関連付けられたカウンタの名前を示す文字列です。

現在、1つのカウンタがサポートされています: "function_entry_count"。2番目のオペランドは、この関数が呼び出された回数を示す64ビットカウンタです(インストルメンテーションベースのプロファイルの場合)。サンプリングベースのプロファイルの場合、このオペランドは関数が呼び出された回数の近似値です。

たとえば、以下のコードでは、関数foo()のインストルメンテーションは、実行時に2,590回呼び出されたことを示しています。

define i32 @foo() !prof !1 {
  ret i32 0
}
!1 = !{!"function_entry_count", i64 2590}

「function_entry_count」に3つ以上のオペランドがある場合、後のオペランドはThinLTOによってインポートする必要がある関数のGUIDです。これは、サンプリングベースのプロファイルによってのみ設定されます。サンプリングベースのプロファイルは、すでにこれらの関数をインポートしてインライン化していたバイナリで収集されたため、プロファイルアノテーションのためにIRがThinLTOバックエンドで一致することを確認する必要があるためです。これをコールサイトにアノテーションできない理由は、コールチェーンで1レベルしか下がれないためです。foo_in_a_cc()->bar_in_b_cc()->baz_in_c_cc()の場合、コールチェーンで2レベル下がり、bar_in_b_ccとbaz_in_c_ccの両方をインポートする必要があります。