26250

以下は、非常に奇妙な動作を示す C++ コードの一部です。奇妙な理由で、(タイミング領域のに) データを並べ替えると、奇跡的にループがほぼ 6 倍速くなります。

#include <algorithm>
#include <ctime>
#include <iostream>

int main()
{
    // Generate data
    const unsigned arraySize = 32768;
    int data[arraySize];

    for (unsigned c = 0; c < arraySize; ++c)
        data[c] = std::rand() % 256;

    // !!! With this, the next loop runs faster.
    std::sort(data, data + arraySize);

    // Test
    clock_t start = clock();
    long long sum = 0;
    for (unsigned i = 0; i < 100000; ++i)
    {
        for (unsigned c = 0; c < arraySize; ++c)
        {   // Primary loop
            if (data[c] >= 128)
                sum += data[c];
        }
    }

    double elapsedTime = static_cast<double>(clock()-start) / CLOCKS_PER_SEC;

    std::cout << elapsedTime << '\n';
    std::cout << "sum = " << sum << '\n';
}
  • がなければstd::sort(data, data + arraySize);、コードは 11.54 秒で実行されます。
  • 並べ替えられたデータを使用すると、コードは 1.93 秒で実行されます。

(並べ替え自体は、配列を 1 回通過するよりも時間がかかるため、未知の配列に対してこれを計算する必要がある場合、実際には行う価値はありません。)


最初は、これは単なる言語またはコンパイラの異常ではないかと考えたので、Java を試してみました。

import java.util.Arrays;
import java.util.Random;

public class Main
{
    public static void main(String[] args)
    {
        // Generate data
        int arraySize = 32768;
        int data[] = new int[arraySize];

        Random rnd = new Random(0);
        for (int c = 0; c < arraySize; ++c)
            data[c] = rnd.nextInt() % 256;

        // !!! With this, the next loop runs faster
        Arrays.sort(data);

        // Test
        long start = System.nanoTime();
        long sum = 0;
        for (int i = 0; i < 100000; ++i)
        {
            for (int c = 0; c < arraySize; ++c)
            {   // Primary loop
                if (data[c] >= 128)
                    sum += data[c];
            }
        }

        System.out.println((System.nanoTime() - start) / 1000000000.0);
        System.out.println("sum = " + sum);
    }
}

同様の結果ですが、それほど極端ではありません。


最初に考えたのは、並べ替えによってデータがキャッシュに取り込まれるということでしたが、配列が生成されたばかりなので、それはどれほどばかげていると思いました。

  • 何が起こっている?
  • 並べ替えられた配列の処理が、並べ替えられていない配列の処理よりも速いのはなぜですか?

コードはいくつかの独立した用語を要約しているため、順序は重要ではありません。


関連/フォローアップの Q&Aは、異なる/新しいコンパイラとオプションでの同じ効果について:

4

28 に答える 28

33859

あなたは分岐予測の失敗の犠牲者です。


分岐予測とは何ですか?

鉄道の分岐点を考えてみましょう。

鉄道の交差点を示す画像 ウィキメディアコモンズ経由のMecanismoによる画像。CC-By-SA3.0ライセンスの下で使用されます。

議論のために、これが1800年代に戻ったとしましょう。遠距離恋愛や無線通信の前です。

あなたはジャンクションの運営者であり、電車が来るのが聞こえます。あなたはそれがどちらの方向に進むべきか見当がつかない。あなたは電車を止めて、運転手にどちらの方向を望むか尋ねます。次に、スイッチを適切に設定します。

列車は重くて慣性が大きいので、発進と減速に永遠にかかります。

もっと良い方法はありますか?あなたは電車がどちらの方向に行くかを推測します!

  • あなたが正しく推測した場合、それは続きます。
  • あなたが間違って推測した場合、船長は停止し、後退し、スイッチを入れるようにあなたに怒鳴ります。その後、他のパスで再起動できます。

毎回正しく推測すれば、電車は止まる必要はありません。
よく間違えると、列車は停車、バックアップ、再開に多くの時間を費やします。


ifステートメントを考えてみましょう。プロセッサレベルでは、これは分岐命令です。

ifステートメントを含むコンパイル済みコードのスクリーンショット

あなたはプロセッサであり、ブランチが表示されます。あなたはそれがどちらの方向に進むのか分かりません。職業はなんですか?実行を停止し、前の命令が完了するまで待ちます。次に、正しいパスを続行します。

最新のプロセッサは複雑で、パイプラインが長くなっています。これは、彼らが「ウォームアップ」と「スローダウン」するのに永遠にかかることを意味します。

もっと良い方法はありますか?あなたは枝がどちらの方向に行くかを推測します!

  • あなたが正しく推測した場合、あなたは実行を続けます。
  • 推測が間違っている場合は、パイプラインをフラッシュしてブランチにロールバックする必要があります。その後、他のパスから再開できます。

毎回正しく推測すれば、実行を停止する必要はありません。
よく間違えると推測すると、ストール、ロールバック、再起動に多くの時間を費やします。


これが分岐予測です。列車は旗で方向を知らせることができるので、これは最良の例えではないことを認めます。しかし、コンピュータでは、プロセッサは最後の瞬間までブランチがどちらの方向に進むかを知りません。

列車が後退して他の経路を下る回数を最小限に抑えるために、戦略的にどのように推測しますか?あなたは過去の歴史を見ます!列車が99%の時間左に行く場合、あなたは左だと思います。それが交互になる場合、あなたはあなたの推測を交互にします。それが3回ごとに一方向に進む場合、あなたは同じことを推測します...

言い換えれば、あなたはパターンを特定し、それに従うことを試みます。これは多かれ少なかれ分岐予測子のしくみです。

ほとんどのアプリケーションには、正常に動作するブランチがあります。したがって、最新の分岐予測は通常、90%を超えるヒット率を達成します。しかし、認識可能なパターンのない予測不可能な分岐に直面した場合、分岐予測は事実上役に立ちません。

さらに読む:ウィキペディアの「分岐予測」の記事


上から示唆されているように、原因は次のifステートメントです。

if (data[c] >= 128)
    sum += data[c];

データが0から255の間で均等に分散されていることに注意してください。データが並べ替えられると、反復のほぼ前半はifステートメントに入りません。その後、それらはすべてifステートメントに入ります。

分岐は何度も同じ方向に連続して進むため、これは分岐予測に非常に適しています。単純な飽和カウンターでも、方向を切り替えた後の数回の反復を除いて、ブランチを正しく予測します。

迅速な視覚化:

T = branch taken
N = branch not taken

data[] = 0, 1, 2, 3, 4, ... 126, 127, 128, 129, 130, ... 250, 251, 252, ...
branch = N  N  N  N  N  ...   N    N    T    T    T  ...   T    T    T  ...

       = NNNNNNNNNNNN ... NNNNNNNTTTTTTTTT ... TTTTTTTTTT  (easy to predict)

ただし、データが完全にランダムである場合、分岐予測子はランダムなデータを予測できないため、役に立たなくなります。したがって、おそらく約50%の誤予測があります(ランダムな推測に勝るものはありません)。

data[] = 226, 185, 125, 158, 198, 144, 217, 79, 202, 118,  14, 150, 177, 182, ...
branch =   T,   T,   N,   T,   T,   T,   T,  N,   T,   N,   N,   T,   T,   T  ...

       = TTNTTTTNTNNTTT ...   (completely random - impossible to predict)

何ができる?

コンパイラーがブランチを条件付き移動に最適化できない場合、パフォーマンスのために可読性を犠牲にすることをいとわないのであれば、いくつかのハックを試すことができます。

交換:

if (data[c] >= 128)
    sum += data[c];

と:

int t = (data[c] - 128) >> 31;
sum += ~t & data[c];

これにより、分岐が削除され、ビット単位の演算に置き換えられます。

(このハックは、元のifステートメントと厳密に同等ではないことに注意してください。ただし、この場合、のすべての入力値に対して有効ですdata[]。)

ベンチマーク:Core i7 920 @ 3.5 GHz

C ++-VisualStudio2010-x64リリース

シナリオ 時間(秒)
分岐-ランダムデータ 11.777
分岐-ソートされたデータ 2.352
ブランチレス-ランダムデータ 2.564
ブランチレス-ソートされたデータ 2.587

Java-NetBeans 7.1.1 JDK 7-x64

シナリオ 時間(秒)
分岐-ランダムデータ 10.93293813
分岐-ソートされたデータ 5.643797077
ブランチレス-ランダムデータ 3.113581453
ブランチレス-ソートされたデータ 3.186068823

観察:

  • ブランチの場合:ソートされたデータとソートされていないデータには大きな違いがあります。
  • ハックの場合:ソートされたデータとソートされていないデータに違いはありません。
  • C ++の場合、データが並べ替えられると、ハッキングは実際にはブランチよりも少し遅くなります。

一般的な経験則は、重要なループ(この例など)でのデータ依存の分岐を回避することです。


アップデート:

  • -O3x64を使用するまたは使用するGCC4.6.1-ftree-vectorizeは条件付き移動を生成できるため、並べ替えられたデータと並べ替えられていないデータに違いはありません。どちらも高速です。

    (またはやや速い:すでにソートされている場合、cmov特にGCCがそれを単にではなくクリティカルパスに置く場合、特に2サイクルの遅延がaddあるBroadwellより前のIntelでは遅くなる可能性があります: gcc最適化フラグ-O3はコードを-O2より遅くします)。cmov

  • VC ++ 2010は、。の下でも、このブランチの条件付き移動を生成できません/Ox

  • インテルC++コンパイラー(ICC)11は、奇跡的なことをします。それは2つのループを交換し、それによって予測できない分岐を外側のループに巻き上げます。誤予測の影響を受けないだけでなく、VC++およびGCCが生成できるものの2倍の速度もあります。言い換えれば、ICCはテストループを利用してベンチマークを打ち負かしました...

  • インテル®コンパイラーにブランチレス・コードを与えると、それは完全にベクトル化されます...そしてブランチと同じくらい高速です(ループ交換を使用)。

これは、成熟した最新のコンパイラでさえ、コードを最適化する能力が大きく異なる可能性があることを示しています...

于 2012-06-27T13:56:42.820 に答える
4465

分岐予測。

ソートされた配列の場合、条件data[c] >= 128は最初falseに一連の値に対してであり、次にそれtrue以降のすべての値に対してになります。それは簡単に予測できます。ソートされていない配列では、分岐コストを支払います。

于 2012-06-27T13:54:45.240 に答える
3623

Mysticial の回答で美しく説明されているように、データを並べ替えるとパフォーマンスが劇的に向上する理由は、分岐予測のペナルティが取り除かれるためです。

さて、コードを見ると

if (data[c] >= 128)
    sum += data[c];

if... else...この特定の分岐の意味は、条件が満たされたときに何かを追加することであることがわかります。このタイプの分岐は、システム内で条件付き移動命令にコンパイルされる条件付き移動ステートメントに簡単に変換できます。分岐、ひいては潜在的な分岐予測ペナルティが取り除かれます。cmovlx86

Cしたがって、ではC++、 の条件付き移動命令に (最適化なしで) 直接コンパイルされるステートメントx86は、 三項演算子... ? ... : ...です。したがって、上記のステートメントを同等のものに書き換えます。

sum += data[c] >=128 ? data[c] : 0;

可読性を維持しながら、高速化の要因を確認できます。

Intel Core i7 -2600K @ 3.4 GHz および Visual Studio 2010 リリース モードでは、ベンチマークは次のとおりです。

x86

シナリオ 時間(秒)
分岐 - ランダム データ 8.885
分岐 - ソートされたデータ 1.528
ブランチレス - ランダム データ 3.716
ブランチレス - ソートされたデータ 3.71

x64

シナリオ 時間(秒)
分岐 - ランダム データ 11.302
分岐 - ソートされたデータ 1.830
ブランチレス - ランダム データ 2.736
ブランチレス - ソートされたデータ 2.737

結果は、複数のテストで堅牢です。分岐の結果が予測できない場合は大幅なスピードアップが得られますが、予測可能な場合は少し問題があります。実際、条件付き移動を使用する場合、パフォーマンスはデータ パターンに関係なく同じです。

x86次に、それらが生成するアセンブリを調査して、より詳しく見てみましょう。簡単にするために、2 つの関数max1とを使用しmax2ます。

max1条件分岐を使用しますif... else ...:

int max1(int a, int b) {
    if (a > b)
        return a;
    else
        return b;
}

max2三項演算子を使用します... ? ... : ...:

int max2(int a, int b) {
    return a > b ? a : b;
}

x86-64 マシンでGCC -Sは、以下のアセンブリを生成します。

:max1
    movl    %edi, -4(%rbp)
    movl    %esi, -8(%rbp)
    movl    -4(%rbp), %eax
    cmpl    -8(%rbp), %eax
    jle     .L2
    movl    -4(%rbp), %eax
    movl    %eax, -12(%rbp)
    jmp     .L4
.L2:
    movl    -8(%rbp), %eax
    movl    %eax, -12(%rbp)
.L4:
    movl    -12(%rbp), %eax
    leave
    ret

:max2
    movl    %edi, -4(%rbp)
    movl    %esi, -8(%rbp)
    movl    -4(%rbp), %eax
    cmpl    %eax, -8(%rbp)
    cmovge  -8(%rbp), %eax
    leave
    ret

max2instruction の使用により、使用するコードがはるかに少なくなりますcmovge。しかし、実際の利点は、予測結果が正しくない場合にパフォーマンスが大幅に低下するmax2ブランチ ジャンプを含まないことです。jmp

では、なぜ条件付き移動のパフォーマンスが向上するのでしょうか?

通常のx86プロセッサでは、命令の実行はいくつかの段階に分割されます。大まかに言えば、さまざまなステージに対応するためにさまざまなハードウェアがあります。したがって、新しい命令を開始するために、1 つの命令が完了するのを待つ必要はありません。これはパイプライン化と呼ばれます。

分岐の場合、前の命令によって次の命令が決まるため、パイプライン化できません。待つか、予測する必要があります。

条件付き移動の場合、実行条件付き移動命令はいくつかのステージに分割されますが、前のステージは前の命令の結果と同様FetchでありDecode、依存しません。後の段階でのみ結果が必要です。したがって、1 つの命令の実行時間の一部を待機します。これが、予測が簡単な場合に条件付き移動バージョンが分岐よりも遅い理由です。

Computer Systems: A Programmer's Perspective の第 2 版では、これについて詳しく説明しています。条件付き移動命令についてはセクション 3.6.6、プロセッサ アーキテクチャについては第 4 章全体、分岐予測と誤予測ペナルティの特別な扱いについてはセクション 5.11.2 を確認できます。

最新のコンパイラの中には、コードをアセンブリに最適化してパフォーマンスを向上できるものもあれば、そうでないものもあります (問題のコードは Visual Studio のネイティブ コンパイラを使用しています)。予測不可能な場合の分岐と条件付き移動のパフォーマンスの違いを知ることは、シナリオが非常に複雑になり、コンパイラがそれらを自動的に最適化できない場合に、より優れたパフォーマンスでコードを記述するのに役立ちます。

于 2012-06-28T02:14:03.600 に答える
2491

このコードに対して実行できるさらに多くの最適化に興味がある場合は、次のことを検討してください。

元のループから始めます。

for (unsigned i = 0; i < 100000; ++i)
{
    for (unsigned j = 0; j < arraySize; ++j)
    {
        if (data[j] >= 128)
            sum += data[j];
    }
}

ループ インターチェンジを使用すると、このループを次のように安全に変更できます。

for (unsigned j = 0; j < arraySize; ++j)
{
    for (unsigned i = 0; i < 100000; ++i)
    {
        if (data[j] >= 128)
            sum += data[j];
    }
}

次に、条件がループifの実行全体で一定であることを確認できるため、 outを巻き上げることができます。iif

for (unsigned j = 0; j < arraySize; ++j)
{
    if (data[j] >= 128)
    {
        for (unsigned i = 0; i < 100000; ++i)
        {
            sum += data[j];
        }
    }
}

次に、浮動小数点モデルで許可されていると仮定すると、内側のループを 1 つの式に折りたたむことができることがわかります (/fp:fastたとえば、 がスローされます)。

for (unsigned j = 0; j < arraySize; ++j)
{
    if (data[j] >= 128)
    {
        sum += data[j] * 100000;
    }
}

それは以前よりも 100,000 倍高速です。

于 2012-07-03T02:25:30.523 に答える
2046

間違いなく、CPUの分岐予測に問題のあるコードを特定する方法に興味を持っている人もいるでしょう。Valgrindツールには、フラグcachegrindを使用して有効化される分岐予測シミュレーターがあります。--branch-sim=yesこの質問の例で実行すると、外部ループの数が10000に減り、でコンパイルされてg++、次の結果が得られます。

並べ替え:

==32551== Branches:        656,645,130  (  656,609,208 cond +    35,922 ind)
==32551== Mispredicts:         169,556  (      169,095 cond +       461 ind)
==32551== Mispred rate:            0.0% (          0.0%     +       1.2%   )

未分類:

==32555== Branches:        655,996,082  (  655,960,160 cond +  35,922 ind)
==32555== Mispredicts:     164,073,152  (  164,072,692 cond +     460 ind)
==32555== Mispred rate:           25.0% (         25.0%     +     1.2%   )

cg_annotate問題のループについて、次のように生成された行ごとの出力にドリルダウンします。

並べ替え:

          Bc    Bcm Bi Bim
      10,001      4  0   0      for (unsigned i = 0; i < 10000; ++i)
           .      .  .   .      {
           .      .  .   .          // primary loop
 327,690,000 10,016  0   0          for (unsigned c = 0; c < arraySize; ++c)
           .      .  .   .          {
 327,680,000 10,006  0   0              if (data[c] >= 128)
           0      0  0   0                  sum += data[c];
           .      .  .   .          }
           .      .  .   .      }

未分類:

          Bc         Bcm Bi Bim
      10,001           4  0   0      for (unsigned i = 0; i < 10000; ++i)
           .           .  .   .      {
           .           .  .   .          // primary loop
 327,690,000      10,038  0   0          for (unsigned c = 0; c < arraySize; ++c)
           .           .  .   .          {
 327,680,000 164,050,007  0   0              if (data[c] >= 128)
           0           0  0   0                  sum += data[c];
           .           .  .   .          }
           .           .  .   .      }

これにより、問題のある行を簡単に特定できます。並べ替えられていないバージョンでは、if (data[c] >= 128)行はcachegrindの分岐予測モデルで164,050,007の誤った予測条件分岐(Bcm)を引き起こしますが、並べ替えられたバージョンでは10,006しか発生しません。


または、Linuxでは、パフォーマンスカウンターサブシステムを使用して同じタスクを実行できますが、CPUカウンターを使用したネイティブパフォーマンスを使用します。

perf stat ./sumtest_sorted

並べ替え:

 Performance counter stats for './sumtest_sorted':

  11808.095776 task-clock                #    0.998 CPUs utilized          
         1,062 context-switches          #    0.090 K/sec                  
            14 CPU-migrations            #    0.001 K/sec                  
           337 page-faults               #    0.029 K/sec                  
26,487,882,764 cycles                    #    2.243 GHz                    
41,025,654,322 instructions              #    1.55  insns per cycle        
 6,558,871,379 branches                  #  555.455 M/sec                  
       567,204 branch-misses             #    0.01% of all branches        

  11.827228330 seconds time elapsed

未分類:

 Performance counter stats for './sumtest_unsorted':

  28877.954344 task-clock                #    0.998 CPUs utilized          
         2,584 context-switches          #    0.089 K/sec                  
            18 CPU-migrations            #    0.001 K/sec                  
           335 page-faults               #    0.012 K/sec                  
65,076,127,595 cycles                    #    2.253 GHz                    
41,032,528,741 instructions              #    0.63  insns per cycle        
 6,560,579,013 branches                  #  227.183 M/sec                  
 1,646,394,749 branch-misses             #   25.10% of all branches        

  28.935500947 seconds time elapsed

また、分解してソースコードの注釈を付けることもできます。

perf record -e branch-misses ./sumtest_unsorted
perf annotate -d sumtest_unsorted
 Percent |      Source code & Disassembly of sumtest_unsorted
------------------------------------------------
...
         :                      sum += data[c];
    0.00 :        400a1a:       mov    -0x14(%rbp),%eax
   39.97 :        400a1d:       mov    %eax,%eax
    5.31 :        400a1f:       mov    -0x20040(%rbp,%rax,4),%eax
    4.60 :        400a26:       cltq   
    0.00 :        400a28:       add    %rax,-0x30(%rbp)
...

詳細については、パフォーマンスチュートリアルを参照してください。

于 2012-10-12T05:53:33.240 に答える
1334

配列がソートされるとデータが 0 から 255 に分散されるため、反復の前半あたりで - ステートメントに入りませんif(ifステートメントは以下で共有されます)。

if (data[c] >= 128)
    sum += data[c];

問題は、ソートされたデータの場合など、特定のケースで上記のステートメントが実行されない理由は何ですか? これが「分岐予測子」です。if-then-else分岐予測器は、分岐 (例:構造) がどの方向に進むかが確定する前に推測しようとするデジタル回路です。分岐予測子の目的は、命令パイプラインの流れを改善することです。分岐予測子は、高い効果的なパフォーマンスを達成する上で重要な役割を果たします!

それをよりよく理解するために、いくつかのベンチマークを行いましょう

-ステートメントのパフォーマンスは、ifその条件に予測可能なパターンがあるかどうかによって異なります。条件が常に true または常に false の場合、プロセッサの分岐予測ロジックがパターンを取得します。一方、パターンが予測できない場合は、ステートメントのifコストがはるかに高くなります。

さまざまな条件でこのループのパフォーマンスを測定してみましょう。

for (int i = 0; i < max; i++)
    if (condition)
        sum++;

さまざまな true-false パターンを使用したループのタイミングを次に示します。

Condition                Pattern             Time (ms)
-------------------------------------------------------
(i & 0×80000000) == 0    T repeated          322

(i & 0xffffffff) == 0    F repeated          276

(i & 1) == 0             TF alternating      760

(i & 3) == 0             TFFFTFFF…           513

(i & 2) == 0             TTFFTTFF…           1675

(i & 4) == 0             TTTTFFFFTTTTFFFF…   1275

(i & 8) == 0             8T 8F 8T 8F …       752

(i & 16) == 0            16T 16F 16T 16F …   490

「<strong>悪い」真偽パターンはif、「<strong>良い」パターンよりも最大 6 倍遅くなります。もちろん、どのパターンが良いか悪いかは、コンパイラによって生成される正確な命令と特定のプロセッサによって異なります。

したがって、分岐予測がパフォーマンスに与える影響に疑いの余地はありません!

于 2013-02-15T07:24:16.033 に答える
1140

ソートされたケースでは、成功した分岐予測や分岐のない比較トリックに頼るよりもうまくいくことができます: 分岐を完全に削除します。

実際、配列は で連続したゾーンに分割され、data < 128別の で分割されdata >= 128ます。したがって、二分探索(Lg(arraySize) = 15比較を使用) で分割点を見つけてから、その点から直接累積を行う必要があります。

のようなもの(チェックなし)

int i= 0, j, k= arraySize;
while (i < k)
{
  j= (i + k) >> 1;
  if (data[j] >= 128)
    k= j;
  else
    i= j;
}
sum= 0;
for (; i < arraySize; i++)
  sum+= data[i];

または、もう少し難読化

int i, k, j= (i + k) >> 1;
for (i= 0, k= arraySize; i < k; (data[j] >= 128 ? k : i)= j)
  j= (i + k) >> 1;
for (sum= 0; i < arraySize; i++)
  sum+= data[i];

ソート済みまたは未ソートの両方の近似解を与える、さらに高速なアプローチは次のとおりsum= 3137536;です。

于 2013-07-24T07:57:39.443 に答える
797

同じ行で(これはどの回答でも強調されていないと思います)、時々(特にLinuxカーネルのようにパフォーマンスが重要なソフトウェアでは)次のようなifステートメントを見つけることができることに言及するのは良いことです:

if (likely( everything_is_ok ))
{
    /* Do something */
}

または同様に:

if (unlikely(very_improbable_condition))
{
    /* Do something */    
}

実際には両方ともlikely()unlikely()GCC のようなものを使用して定義されたマクロであり__builtin_expect、コンパイラが予測コードを挿入して、ユーザーから提供された情報を考慮して条件を優先するのに役立ちます。GCC は、実行中のプログラムの動作を変更したり、キャッシュのクリアなどの低レベルの命令を発行したりできるその他のビルトインをサポートしています。利用可能な GCC のビルトインについては、このドキュメントを参照してください。

通常、この種の最適化は、主に実行時間が重要で重要なハードリアルタイム アプリケーションまたは組み込みシステムで見られます。たとえば、1/10000000 回しか発生しないエラー状態をチェックしている場合、これについてコンパイラに通知しないのはなぜでしょうか? このように、デフォルトでは、分岐予測は条件が false であると想定します。

于 2015-09-23T14:57:47.163 に答える
781

C++ で頻繁に使用されるブール演算は、コンパイルされたプログラムで多くの分岐を生成します。これらの分岐がループ内にあり、予測が困難な場合、実行が大幅に遅くなる可能性があります。ブール変数は、およびの値0を持つ 8 ビット整数として格納されます。false1true

ブール変数は、ブール変数を入力として持つすべての演算子が入力が0or以外の値を持つかどうかをチェックするという意味で過剰決定されますが、ブール変数を出力として持つ演算子はor1以外の値を生成できません。これにより、ブール変数を入力として使用する操作が、必要以上に効率的ではなくなります。例を考えてみましょう:01

bool a, b, c, d;
c = a && b;
d = a || b;

これは通常、コンパイラによって次の方法で実装されます。

bool a, b, c, d;
if (a != 0) {
    if (b != 0) {
        c = 1;
    }
    else {
        goto CFALSE;
    }
}
else {
    CFALSE:
    c = 0;
}
if (a == 0) {
    if (b == 0) {
        d = 0;
    }
    else {
        goto DTRUE;
    }
}
else {
    DTRUE:
    d = 1;
}

このコードは最適とはほど遠いものです。予測に誤りがあると、分岐に時間がかかる場合があります。0ブール演算は、オペランドがおよび以外の値を持たないことが確実にわかっている場合、はるかに効率的にすることができます1。コンパイラがそのような仮定を行わない理由は、変数が初期化されていないか、未知のソースからのものである場合、変数が他の値を持つ可能性があるためです。上記のコードは、有効な値に初期化されている場合、またはブール出力を生成する演算子からのものである場合に最適化できaますb。最適化されたコードは次のようになります。

char a = 0, b = 1, c, d;
c = a & b;
d = a | b;

charブール演算子 ( および ) の代わりにビット演算子 (および)boolを使用できるようにするために、の代わりに が使用されます。ビット演算子は、1 クロック サイクルしかかからない単一の命令です。OR 演算子 ( ) は、andがor以外の値であっても機能します。AND 演算子 ( ) および EXCLUSIVE OR 演算子 ( ) は、オペランドがおよび以外の値を持つ場合、矛盾する結果を返す可能性があります。&|&&|||ab01&^01

~NOT には使用できません。代わりに、次のように XOR する0か、変数であることがわかっている変数に対してブール値の NOT を作成できます。11

bool a, b;
b = !a;

次のように最適化できます。

char a = 0, b;
b = a ^ 1;

a && ba & bif is is is ( will not evaluate , will )の場合はb評価されるべきではない式です。同様に、is の場合に評価されるべきではない式であるif isに置き換えることはできません。afalse&&b&a || ba | bbatrue

オペランドが変数である場合は、オペランドが比較である場合よりも、ビットごとの演算子を使用する方が有利です。

bool a; double x, y, z;
a = x > y && z < 5.0;

&&式が多くの分岐予測ミスを生成することが予想される場合を除き、ほとんどの場合に最適です。

于 2015-10-10T00:30:42.687 に答える
205

分岐予測によって速度が低下する可能性があるという事実に加えて、並べ替えられた配列には別の利点があります。

値をチェックするだけでなく、停止条件を設定することもできます。この方法では、関連するデータのみをループし、残りは無視します。
分岐予測は一度だけ外れます。

 // sort backwards (higher values first), may be in some other part of the code
 std::sort(data, data + arraySize, std::greater<int>());

 for (unsigned c = 0; c < arraySize; ++c) {
       if (data[c] < 128) {
              break;
       }
       sum += data[c];               
 }
于 2017-11-23T14:28:29.353 に答える
173

次の MATLAB コードについて、MacBook Pro (Intel i7、64 ビット、2.4 GHz) を搭載した MATLAB 2011b で同じコードを試しました。

% Processing time with Sorted data vs unsorted data
%==========================================================================
% Generate data
arraySize = 32768
sum = 0;
% Generate random integer data from range 0 to 255
data = randi(256, arraySize, 1);


%Sort the data
data1= sort(data); % data1= data  when no sorting done


%Start a stopwatch timer to measure the execution time
tic;

for i=1:100000

    for j=1:arraySize

        if data1(j)>=128
            sum=sum + data1(j);
        end
    end
end

toc;

ExeTimeWithSorting = toc - tic;

上記の MATLAB コードの結果は次のとおりです。

  a: Elapsed time (without sorting) = 3479.880861 seconds.
  b: Elapsed time (with sorting ) = 2377.873098 seconds.

@GManNickG のような C コードの結果:

  a: Elapsed time (without sorting) = 19.8761 sec.
  b: Elapsed time (with sorting ) = 7.37778 sec.

これに基づくと、MATLAB は並べ替えなしの C 実装よりも約175 倍遅く、並べ替えありの場合は350 倍遅いようです。つまり、(分岐予測の) 効果は、MATLAB 実装で1.46、C 実装で 2.7 倍です。

于 2012-12-30T16:16:46.597 に答える
87

この質問は、CPUの分岐予測モデルに根ざしています。この論文を読むことをお勧めします:

複数分岐予測と分岐アドレスキャッシュによる命令フェッチレートの向上

要素を並べ替えた場合、IRはすべての CPU 命令を何度もフェッチする必要はありません。キャッシュからそれらをフェッチします。

于 2019-10-23T21:35:40.250 に答える