コード変換メタデータ

概要

LLVM変換パスは、変換するコードにメタデータを付加することで制御できます。デフォルトでは、変換パスはヒューリスティックを使用して変換を実行するかどうかを決定し、実行する場合は、変換を適用する方法の他の詳細(たとえば、選択するベクトル化係数など)を決定します。オプティマイザーが別途指示されない限り、変換は保守的に適用されます。この保守性により、一般的にオプティマイザーは非効率な変換を回避できますが、実際には、オプティマイザーが非常に有益な変換を適用しないという結果になります。

フロントエンドは、適用するべき変換に関して、LLVMパスに追加のヒントを与えることができます。これは、出力されたIRから導出できない追加の知識、またはユーザー/プログラマーから渡されたディレクティブである場合があります。OpenMPプラグマは後者の例です。

このようなメタデータがプログラムから削除された場合、コードのセマンティクスは変更されないはずです。

ループに関するメタデータ

「llvm.loop」に記載されているように、属性をループに付加できます。属性は、ループのプロパティを記述したり、変換を無効にしたり、特定の変換を強制したり、変換オプションを設定したりできます。

メタデータノードは不変であるため(一意のメタデータで使用するのは危険なMDNode::replaceOperandWithは例外)、ループ属性を追加または削除するには、新しいMDNodeを作成し、新しいllvm.loopメタデータとして割り当てる必要があります。古いMDNodeとループ間の接続は失われます。llvm.loopノードはLoopID(Loop::getLoopID())としても使用されます。つまり、ループには実質的に新しい識別子が与えられます。たとえば、llvm.mem.parallel_loop_accessはLoopIDを参照します。したがって、ループ属性を追加/削除した後で並列アクセスプロパティを保持する場合は、llvm.mem.parallel_loop_access参照を新しいLoopIDに更新する必要があります。

変換メタデータの構造

一部の属性は、コード変換(アンローリング、ベクトル化、ループ分散など)を記述します。これらは、変換が有益である可能性のあるオプティマイザーへのヒント、特定のオプションを使用する指示、またはユーザーからの特定のリクエスト(#pragma clang loop#pragma omp simdなど)を伝えることができます。

変換が強制されているにもかかわらず、何らかの理由で実行できない場合は、最適化が見送られたという警告を出す必要があります。変換が安全である(たとえば、llvm.mem.parallel_loop_access)などのセマンティック情報は、警告を生成することなくオプティマイザーで使用されない場合があります。

明示的に無効にしない限り、任意の最適化パスは、変換が有益かどうかをヒューリスティックに判断して適用する場合があります。別の変換のメタデータが指定されている場合、異なるループに適用されたり、ループがもはや存在しないために、その前に別の変換を適用すると、意図しない結果になる可能性があります。不明な数のパスを明示的に無効にする必要がないように、属性llvm.loop.disable_nonforcedは、すべてのオプションの、高レベルの再構築変換を無効にします。

次の例では、ループがベクトル化される前に変更されるのを防ぎます(たとえば、アンロールされる)。

  br i1 %exitcond, label %for.exit, label %for.header, !llvm.loop !0
...
!0 = distinct !{!0, !1, !2}
!1 = !{!"llvm.loop.vectorize.enable", i1 true}
!2 = !{!"llvm.loop.disable_nonforced"}

変換が適用されたら、追跡属性は変換されたループまたは新しいループ(またはその両方)に設定されます。これにより、追跡変換を含む追加の属性を指定できます。互換性の理由から、同じメタデータノードに複数の変換を指定できますが、それらの実行順序は定義されていません。たとえば、llvm.loop.vectorize.enablellvm.loop.unroll.enableが同時に指定されている場合、アンローリングはベクトル化の前または後のどちらかで発生する可能性があります。

例として、次の例では、ループをベクトル化してからアンロールするように指示しています。

!0 = distinct !{!0, !1, !2, !3}
!1 = !{!"llvm.loop.vectorize.enable", i1 true}
!2 = !{!"llvm.loop.disable_nonforced"}
!3 = !{!"llvm.loop.vectorize.followup_vectorized", !{"llvm.loop.unroll.enable"}}

フォローアップが指定されていない場合に限り、パス自体が属性を追加できます。たとえば、ベクタライザーはllvm.loop.isvectorized属性と、ループベクタライザー属性を除く元のループからのすべての属性を追加します。これを回避するには、空のフォローアップ属性を使用できます。例:

!3 = !{!"llvm.loop.vectorize.followup_vectorized"}

適用できない変換のフォローアップ属性は、ループに追加されることはなく、そのため事実上無視されます。これは、このような属性のフォローアップ変換は、その前の変換がフォローアップ変換の前に適用される必要があることを意味します。強制変換の場合、適用できなかった変換チェーンの最初の変換について、ユーザーは警告を受け取る必要があります。後続のすべての変換はスキップされます。

パス固有の変換メタデータ

変換オプションは、各変換に固有です。以下では、各LLVMループ最適化パスのモデルと、それらに影響を与えるメタデータを示します。

ループのベクトル化とインターリーブ

ループのベクトル化とインターリーブは、単一の変換として解釈されます。!{"llvm.loop.vectorize.enable", i1 true}が設定されている場合、強制と解釈されます。

ベクトル化前のループが次のような場合

for (int i = 0; i < n; i+=1) // original loop
  Stmt(i);

ベクトル化後のコードはほぼ次のようになります(SIMD幅が4の場合)

int i = 0;
if (rtc) {
  for (; i + 3 < n; i+=4) // vectorized/interleaved loop
    Stmt(i:i+3);
}
for (; i < n; i+=1) // epilogue loop
  Stmt(i);

ここで、rtcは生成されたランタイムチェックです。

llvm.loop.vectorize.followup_vectorizedは、ベクトル化されたループの属性を設定します。指定しない場合、llvm.loop.isvectorizedは元のループの属性と組み合わされて、複数回ベクトル化されるのを防ぎます。

llvm.loop.vectorize.followup_epilogueは、残りのループの属性を設定します。指定しない場合、元のループの属性とllvm.loop.isvectorizedおよびllvm.loop.unroll.runtime.disableが組み合わされます(元のループにアンロールメタデータが既にある場合を除く)。

llvm.loop.vectorize.followup_allで指定された属性は、両方のループに追加されます。

フォローアップ属性を使用する場合、それは問題の生成されたループに対して自動的に推論された属性を置き換えます。したがって、llvm.loop.isvectorizedllvm.loop.vectorize.followup_allに追加することをお勧めします。これにより、ループベクタライザーがループを再度最適化しようとするのを防ぐことができます。

ループのアンローリング

アンローリングは、!{!"llvm.loop.unroll.enable"}メタデータまたはオプション(llvm.loop.unroll.countllvm.loop.unroll.full)が存在する場合、強制と解釈されます。アンローリングは、完全なアンローリング、一定のトリップカウントを持つループの部分的なアンローリング、またはコンパイル時に不明なトリップカウントを持つループのランタイムアンローリングにすることができます。

ループが完全にアンロールされている場合、フォローアップループはありません。部分/ランタイムアンローリングの場合、次の元のループ

for (int i = 0; i < n; i+=1) // original loop
  Stmt(i);

は(アンロール係数4を使用)、次のように変換されます

int i = 0;
for (; i + 3 < n; i+=4) { // unrolled loop
  Stmt(i);
  Stmt(i+1);
  Stmt(i+2);
  Stmt(i+3);
}
for (; i < n; i+=1) // remainder loop
  Stmt(i);

llvm.loop.unroll.followup_unrolledは、アンロールされたループのループ属性を設定します。指定しない場合、llvm.loop.unroll.*属性のない元のループの属性がコピーされ、llvm.loop.unroll.disableが追加されます。

llvm.loop.unroll.followup_remainderは、残りのループの属性を定義します。指定しない場合、残りのループには属性がありません。残りのループは完全にアンロールされているために存在しない場合があり、その場合、この属性は効果がありません。

llvm.loop.unroll.followup_allで定義された属性は、アンロールされたループと残りのループに追加されます。

部分的にアンロールされたループが再びアンロールされるのを防ぐために、llvm.loop.unroll.disablellvm.loop.unroll.followup_allに追加することをお勧めします。生成されたループにフォローアップ属性が指定されていない場合は、自動的に追加されます。

アンロールアンドジャム

アンロールアンドジャムは、次の変換モデルを使用します(ここではアンロール係数が2の場合)。現在、変換が安全でない場合のフォールバックバージョンはサポートされていません。

for (int i = 0; i < n; i+=1) { // original outer loop
  Fore(i);
  for (int j = 0; j < m; j+=1) // original inner loop
    SubLoop(i, j);
  Aft(i);
}
int i = 0;
for (; i + 1 < n; i+=2) { // unrolled outer loop
  Fore(i);
  Fore(i+1);
  for (int j = 0; j < m; j+=1) { // unrolled inner loop
    SubLoop(i, j);
    SubLoop(i+1, j);
  }
  Aft(i);
  Aft(i+1);
}
for (; i < n; i+=1) { // remainder outer loop
  Fore(i);
  for (int j = 0; j < m; j+=1) // remainder inner loop
    SubLoop(i, j);
  Aft(i);
}

llvm.loop.unroll_and_jam.followup_outerは、アンロールされた外側のループのループ属性を設定します。指定しない場合、llvm.loop.unroll.*属性のない元の外側のループの属性がコピーされ、llvm.loop.unroll.disableが追加されます。

llvm.loop.unroll_and_jam.followup_innerは、アンロールされた内側のループのループ属性を設定します。指定しない場合、元の内側のループの属性が変更されずに使用されます。

llvm.loop.unroll_and_jam.followup_remainder_outer は、外側の剰余ループのループ属性を設定します。指定しない場合、属性は設定されません。剰余ループは、完全にアンロールされているため存在しない場合があります。

llvm.loop.unroll_and_jam.followup_remainder_inner は、内側の剰余ループのループ属性を設定します。指定しない場合、元の内側ループの属性が設定されます。外側の剰余ループがアンロールされている場合、内側の剰余ループが複数回存在する可能性があります。

llvm.loop.unroll_and_jam.followup_all で定義された属性は、上記の出力ループすべてに追加されます。

アンロールされたループが再度アンロールされるのを避けるため、llvm.loop.unroll.disablellvm.loop.unroll_and_jam.followup_all に追加することをお勧めします。これにより、アンロールアンドジャムだけでなく、追加の内側ループのアンロールも抑制されます。生成されたループにフォローアップ属性が指定されていない場合、自動的に追加されます。

ループ分散

ループ分散パスは、ループ内でベクトル化可能な部分をベクトル化できない部分(そうでないとループ全体がベクトル化不可能になる)から分離しようとします。概念的には、次のようなループを変換します。

for (int i = 1; i < n; i+=1) { // original loop
  A[i] = i;
  B[i] = 2 + B[i];
  C[i] = 3 + C[i - 1];
}

次のコードに変換します。

if (rtc) {
  for (int i = 1; i < n; i+=1) // coincident loop
    A[i] = i;
  for (int i = 1; i < n; i+=1) // coincident loop
    B[i] = 2 + B[i];
  for (int i = 1; i < n; i+=1) // sequential loop
    C[i] = 3 + C[i - 1];
} else {
  for (int i = 1; i < n; i+=1) { // fallback loop
    A[i] = i;
    B[i] = 2 + B[i];
    C[i] = 3 + C[i - 1];
  }
}

ここで、rtcは生成されたランタイムチェックです。

llvm.loop.distribute.followup_coincident は、ループ依存を持たないすべてのループ(つまり、ベクトル化可能なループ)のループ属性を設定します。このようなループは複数存在する可能性があります。定義されていない場合、ループは元のループの属性を継承します。

llvm.loop.distribute.followup_sequential は、潜在的に安全でない依存関係を持つループのループ属性を設定します。このようなループは最大で1つである必要があります。定義されていない場合、ループは元のループの属性を継承します。

llvm.loop.distribute.followup_fallback は、ループのバージョン管理が必要な場合に、元のループのコピーであるフォールバックループのループ属性を定義します。未定義の場合、フォールバックループは元のループからすべての属性を継承します。

llvm.loop.distribute.followup_all で定義された属性は、上記の出力ループすべてに追加されます。

llvm.loop.disable_nonforcedllvm.loop.distribute.followup_fallback に追加することをお勧めします。これにより、フォールバックバージョン(おそらく実行されない)がさらに最適化され、コードサイズが増加するのを防ぎます。

バージョニングLICM

このパスは、動的な条件が適用される場合にのみループ不変となるコードをループの外に移動させます。たとえば、次のようなループを変換します。

for (int i = 0; i < n; i+=1) // original loop
  A[i] = B[0];

次のように変換します。

if (rtc) {
  auto b = B[0];
  for (int i = 0; i < n; i+=1) // versioned loop
    A[i] = b;
} else {
  for (int i = 0; i < n; i+=1) // unversioned loop
    A[i] = B[0];
}

ランタイム条件(rtc)は、配列Aと要素B[0]がエイリアスしないことを確認します。

現在、この変換はフォローアップ属性をサポートしていません。

ループ交換

現在、LoopInterchange パスはメタデータを使用していません。

曖昧な変換順序

複数の変換が定義されている場合、それらが実行される順序はLLVMのパスパイプラインの順序に依存し、これは変更される可能性があります。デフォルトの最適化パイプライン(-O0 より大きいもの)には、次の順序があります。

レガシーパスマネージャーを使用する場合

  • LoopInterchange (有効な場合)

  • SimpleLoopUnroll/LoopFullUnroll (完全アンローリングのみを実行)

  • VersioningLICM (有効な場合)

  • LoopDistribute

  • LoopVectorizer

  • LoopUnrollAndJam (有効な場合)

  • LoopUnroll (部分アンローリングとランタイムアンローリング)

LTOでレガシーパスマネージャーを使用する場合

  • LoopInterchange (有効な場合)

  • SimpleLoopUnroll/LoopFullUnroll (完全アンローリングのみを実行)

  • LoopVectorizer

  • LoopUnroll (部分アンローリングとランタイムアンローリング)

新しいパスマネージャーを使用する場合

  • SimpleLoopUnroll/LoopFullUnroll (完全アンローリングのみを実行)

  • LoopDistribute

  • LoopVectorizer

  • LoopUnrollAndJam (有効な場合)

  • LoopUnroll (部分アンローリングとランタイムアンローリング)

残りの変換

最後の変換パス後に適用されていない強制変換は、ユーザーに報告する必要があります。変換パス自体は、パイプラインに存在しない可能性、変換を適用できるパスが複数存在する可能性(例:LoopInterchangeとPolly)、または変換属性が別のパスのフォローアップ属性の中に「隠されている」可能性があるため、この報告を担当することはできません。

パス-transform-warning (WarnMissedTransformationsPass) は、このような警告を発行します。これは、最後の変換パスの後に配置する必要があります。

現在のパスパイプラインには、変換パスが実行される固定の順序があります。変換は、後で実行されるパスのフォローアップになる可能性があり、したがって残っている可能性があります。たとえば、現在のパスパイプラインでは、ループネストを分散してから交換することはできません。ループ分散は実行されますが、ループ交換メタデータが無視されるようなループ交換パスは後続にありません。-transform-warning は、この場合に警告を発行する必要があります。

将来のバージョンのLLVMでは、動的な順序を使用して変換を実行することにより、これを修正する可能性があります。