web-dev-qa-db-ja.com

ループ反転技術とは何ですか?

Javaの ジャストインタイムコンパイラ (JIT)最適化手法について説明しているドキュメントを調べていました。それらの1つは「ループ反転」でした。そして文書は言う:

通常のwhileループをdo-whileループに置き換えます。また、do-whileループはif句内で設定されます。この置換により、ジャンプが2つ少なくなります。

ループ反転はどのように機能し、コードパスを最適化しますか?

NB:Javaコードの例で誰かが説明できたら素晴らしいですそして、JITがネイティブコードに最適化する方法と、最新のプロセッサーで最適化される理由

89
Trying
while (condition) { 
  ... 
}

ワークフロー:

  1. チェック状態;
  2. falseの場合、ループの外側にジャンプします。
  3. 1回の反復を実行します。
  4. 先頭にジャンプします。

if (condition) do {
  ...
} while (condition);

ワークフロー:

  1. チェック状態;
  2. falseの場合、ループを超えてジャンプします。
  3. 1回の反復を実行します。
  4. チェック状態;
  5. trueの場合、手順3に進みます。

これら2つを比較すると、ループ全体で1つのステップしかなく、ジャンプの数が反復数より1つ少ない場合、後者はジャンプをまったく行わない可能性があることが簡単にわかります。前者は条件をチェックするためにジャンプして戻る必要があり、条件が偽の場合にループからジャンプするだけです。

最新のパイプライン化されたCPUアーキテクチャでのジャンプは非常に高価になる可能性があります。CPUはジャンプ前にチェックの実行を終了しているため、そのジャンプを超える命令はすでにパイプラインの途中にあります。分岐予測が失敗した場合、この処理はすべて破棄する必要があります。パイプラインが再準備されている間、それ以上の実行は遅延されます。

上記の分岐予測の説明:条件付きジャンプの種類ごとに、CPUには2命令があり、それぞれに結果にbetが含まれています。たとえば、最後の反復を除くすべての反復でジャンプを行う必要があるため、ループの最後に「ゼロでない場合はジャンプ、非ゼロに賭ける」という命令を配置します。このようにして、CPUは、ジャンプ命令自体に続く命令ではなく、ジャンプターゲットに続く命令でパイプラインのポンプを開始します。

重要な注意点

ソースコードレベルで最適化する方法の例として、notを実行してください。最初の形式から2番目の形式への変換は、JITコンパイラーがルーチンの問題として完全に独自に行うものであるため、それは完全に見当違いです。

107
Marko Topolnik

これにより、常に少なくとも1回は実行されるループを最適化できます。

通常のwhileループは、常に少なくとも1回は最初に戻り、最後に1回は最後にジャンプします。 1回実行する単純なループの例:

int i = 0;
while (i++ < 1) {
    //do something
}  

一方、do-whileループは、最初と最後のジャンプをスキップします。以下は、ジャンプなしで実行される上記のループと同等のループです。

int i = 0;
if (i++ < 1) {
    do {
        //do something
    } while (i++ < 1); 
}
24
Keppil

ループ反転は、プロセッサーがより少ない命令で同じ結果を達成できるため、パフォーマンスを改善するパフォーマンス最適化手法です。これにより、境界条件でのパフォーマンスが向上するはずです。

このリンク は、ループ反転の別の例を提供します。デクリメントと比較が単一の命令セットとして実装されているいくつかのアーキテクチャでは、デクリメントと比較の操作でforループをwhileに変換することは理にかなっています。

ウィキペディアには非常に良い例があり、ここでもう一度説明します。

 int i, a[100];
  i = 0;
  while (i < 100) {
    a[i] = 0;
    i++;
  }

コンパイラによって次のように変換されます

  int i, a[100];
  i = 0;
  if (i < 100) {
    do {
      a[i] = 0;
      i++;
    } while (i < 100);
  }

これはどのようにパフォーマンスに変換されますか? iの値が99の場合、プロセッサはGOTO(最初のケースで必要)を実行する必要がありません。これにより、パフォーマンスが向上します。

3
Anirudhan J

それらを見ていきましょう:

whileバージョン:

_void foo(int n) {
    while (n < 10) {
       use(n);
       ++n;
    }
    done();
}
_
  1. 最初にnをテストし、条件が真でない場合はdone();にジャンプします。
  2. 次に、nを使用して増分します。
  3. 次に、条件に戻ります。
  4. すすぎ、繰り返します。
  5. 条件が真でなくなったら、done()にジャンプします。

_do-while_バージョン:

(実際には、これはソースコードでは行われません[メンテナンスの問題が発生する可能性があります]、コンパイラ/ JITが行います)

_void foo(int n) {
    if (n < 10) {
        do {
            use(n);
            ++n;
        }
        while (n < 10);
    }
    done();
}
_
  1. 最初にnをテストし、条件が真でない場合はdone();にジャンプします。
  2. 次に、nを使用して増分します。
  3. 次に、条件をテストし、それが真の場合はジャンプして戻ります。
  4. すすぎ、繰り返します。
  5. 条件がtrueでなくなると、done()に(ジャンプせずに)フローします。

したがって、たとえば、nが_9_で始まる場合、_do-while_バージョンではまったくジャンプしませんが、whileバージョンでは、最初に、テストを行い、それが真実ではないとわかったら最後に戻ります。

3
T.J. Crowder