要素ごとの加算が、結合されたループよりも個別のループではるかに速いのはなぜですか?


2246

仮定a1b1c1、およびd1ヒープメモリと私の数値コードのポイントは、以下のコアループを有しています。

const int n = 100000;

for (int j = 0; j < n; j++) {
    a1[j] += b1[j];
    c1[j] += d1[j];
}

このループは、別の外部forループを介して10,000回実行されます。スピードアップするために、コードを次のように変更しました。

for (int j = 0; j < n; j++) {
    a1[j] += b1[j];
}

for (int j = 0; j < n; j++) {
    c1[j] += d1[j];
}

Intel Core 2 Duo(x64)で32ビットに対して完全最適化およびSSE2を有効にしてMS Visual C ++ 10.0でコンパイルすると、最初の例は5.5秒かかり、ダブルループの例はわずか1.9秒かかります。私の質問は:(下部にある私の言い換えられた質問を参照してください)

PS:これが役立つかどうかはわかりません:

最初のループの逆アセンブリは基本的に次のようになります(このブロックはプログラム全体で約5回繰り返されます)。

movsd       xmm0,mmword ptr [edx+18h]
addsd       xmm0,mmword ptr [ecx+20h]
movsd       mmword ptr [ecx+20h],xmm0
movsd       xmm0,mmword ptr [esi+10h]
addsd       xmm0,mmword ptr [eax+30h]
movsd       mmword ptr [eax+30h],xmm0
movsd       xmm0,mmword ptr [edx+20h]
addsd       xmm0,mmword ptr [ecx+28h]
movsd       mmword ptr [ecx+28h],xmm0
movsd       xmm0,mmword ptr [esi+18h]
addsd       xmm0,mmword ptr [eax+38h]

二重ループの例の各ループはこのコードを生成します(次のブロックは約3回繰り返されます)。

addsd       xmm0,mmword ptr [eax+28h]
movsd       mmword ptr [eax+28h],xmm0
movsd       xmm0,mmword ptr [ecx+20h]
addsd       xmm0,mmword ptr [eax+30h]
movsd       mmword ptr [eax+30h],xmm0
movsd       xmm0,mmword ptr [ecx+28h]
addsd       xmm0,mmword ptr [eax+38h]
movsd       mmword ptr [eax+38h],xmm0
movsd       xmm0,mmword ptr [ecx+30h]
addsd       xmm0,mmword ptr [eax+40h]
movsd       mmword ptr [eax+40h],xmm0

動作は配列(n)とCPUキャッシュのサイズに大きく依存するため、問題は無関係であることがわかりました。したがって、さらに関心がある場合は、質問を言い換えます。

次のグラフの5つの領域で示されているように、さまざまなキャッシュ動作につながる詳細への確かな洞察を提供できますか?

これらのCPUについて同様のグラフを提供することにより、CPU /キャッシュアーキテクチャの違いを指摘することも興味深いかもしれません。

PPS:これが完全なコードです。マクロを定義しないことで無効にできる、より高い解像度のタイミングにTBB Tick_Countを使用しますTBB_TIMING

#include <iostream>
#include <iomanip>
#include <cmath>
#include <string>

//#define TBB_TIMING

#ifdef TBB_TIMING   
#include <tbb/tick_count.h>
using tbb::tick_count;
#else
#include <time.h>
#endif

using namespace std;

//#define preallocate_memory new_cont

enum { new_cont, new_sep };

double *a1, *b1, *c1, *d1;


void allo(int cont, int n)
{
    switch(cont) {
      case new_cont:
        a1 = new double[n*4];
        b1 = a1 + n;
        c1 = b1 + n;
        d1 = c1 + n;
        break;
      case new_sep:
        a1 = new double[n];
        b1 = new double[n];
        c1 = new double[n];
        d1 = new double[n];
        break;
    }

    for (int i = 0; i < n; i++) {
        a1[i] = 1.0;
        d1[i] = 1.0;
        c1[i] = 1.0;
        b1[i] = 1.0;
    }
}

void ff(int cont)
{
    switch(cont){
      case new_sep:
        delete[] b1;
        delete[] c1;
        delete[] d1;
      case new_cont:
        delete[] a1;
    }
}

double plain(int n, int m, int cont, int loops)
{
#ifndef preallocate_memory
    allo(cont,n);
#endif

#ifdef TBB_TIMING   
    tick_count t0 = tick_count::now();
#else
    clock_t start = clock();
#endif

    if (loops == 1) {
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++){
                a1[j] += b1[j];
                c1[j] += d1[j];
            }
        }
    } else {
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                a1[j] += b1[j];
            }
            for (int j = 0; j < n; j++) {
                c1[j] += d1[j];
            }
        }
    }
    double ret;

#ifdef TBB_TIMING   
    tick_count t1 = tick_count::now();
    ret = 2.0*double(n)*double(m)/(t1-t0).seconds();
#else
    clock_t end = clock();
    ret = 2.0*double(n)*double(m)/(double)(end - start) *double(CLOCKS_PER_SEC);
#endif

#ifndef preallocate_memory
    ff(cont);
#endif

    return ret;
}


void main()
{   
    freopen("C:\\test.csv", "w", stdout);

    char *s = " ";

    string na[2] ={"new_cont", "new_sep"};

    cout << "n";

    for (int j = 0; j < 2; j++)
        for (int i = 1; i <= 2; i++)
#ifdef preallocate_memory
            cout << s << i << "_loops_" << na[preallocate_memory];
#else
            cout << s << i << "_loops_" << na[j];
#endif

    cout << endl;

    long long nmax = 1000000;

#ifdef preallocate_memory
    allo(preallocate_memory, nmax);
#endif

    for (long long n = 1L; n < nmax; n = max(n+1, long long(n*1.2)))
    {
        const long long m = 10000000/n;
        cout << n;

        for (int j = 0; j < 2; j++)
            for (int i = 1; i <= 2; i++)
                cout << s << plain(n, m, j, i);
        cout << endl;
    }
}

(のさまざまな値に対するFLOP / sを示していますn。)

ここに画像の説明を入力してください


4
アクセスするたびに物理メモリを検索する際に速度が低下し、同じmemblockへの2次アクセスの場合にキャッシュのようなものがあるオペレーティングシステムである可能性があります。
AlexTheo

7
最適化してコンパイルしていますか?それはO2の多くのasmコードのように見えます...
Luchian Grigore 2011

1
先ほど、ような質問をしました。それまたは回答には、興味のある情報が含まれている場合があります。
Mark Wilkins、2011

61
厳選するために、これら2つのコードスニペットは、ポインタが重複する可能性があるため、同等ではありません。C99はそのrestrictような状況のためのキーワードを持っています。MSVCに同様の機能があるかどうかはわかりません。もちろん、これが問題である場合、SSEコードは正しくありません。
user510306 2011

8
これは、メモリのエイリアシングに関係している可能性があります。1つのループでd1[j]は、とエイリアスになる可能性がa1[j]あるため、コンパイラは一部のメモリ最適化の実行から撤回する場合があります。ただし、メモリへの書き込みを2つのループに分けた場合、それは起こりません。
rturrado 2011

回答:


1690

これをさらに分析すると、これは(少なくとも部分的には)4ポインタのデータ配置が原因であると思います。これにより、ある程度のキャッシュバンク/ウェイの競合が発生します。

配列をどのように割り当てるかを私が正しく推測した場合、それらはページ行に揃えられる可能性があります

これは、各ループでのすべてのアクセスが同じキャッシュ方法に分類されることを意味します。ただし、Intelプロセッサにはしばらくの間、8ウェイL1キャッシュの関連付けがありました。しかし、実際には、パフォーマンスは完全に均一ではありません。4ウェイへのアクセスは、2ウェイと言うよりも遅いです。

編集:実際には、すべての配列を個別に割り当てているように見えます。 通常、このような大きな割り当てが要求されると、アロケータはOSに新しいページを要求します。したがって、ページ境界からの同じオフセットに大きな割り当てが表示される可能性が高くなります。

これがテストコードです:

int main(){
    const int n = 100000;

#ifdef ALLOCATE_SEPERATE
    double *a1 = (double*)malloc(n * sizeof(double));
    double *b1 = (double*)malloc(n * sizeof(double));
    double *c1 = (double*)malloc(n * sizeof(double));
    double *d1 = (double*)malloc(n * sizeof(double));
#else
    double *a1 = (double*)malloc(n * sizeof(double) * 4);
    double *b1 = a1 + n;
    double *c1 = b1 + n;
    double *d1 = c1 + n;
#endif

    //  Zero the data to prevent any chance of denormals.
    memset(a1,0,n * sizeof(double));
    memset(b1,0,n * sizeof(double));
    memset(c1,0,n * sizeof(double));
    memset(d1,0,n * sizeof(double));

    //  Print the addresses
    cout << a1 << endl;
    cout << b1 << endl;
    cout << c1 << endl;
    cout << d1 << endl;

    clock_t start = clock();

    int c = 0;
    while (c++ < 10000){

#if ONE_LOOP
        for(int j=0;j<n;j++){
            a1[j] += b1[j];
            c1[j] += d1[j];
        }
#else
        for(int j=0;j<n;j++){
            a1[j] += b1[j];
        }
        for(int j=0;j<n;j++){
            c1[j] += d1[j];
        }
#endif

    }

    clock_t end = clock();
    cout << "seconds = " << (double)(end - start) / CLOCKS_PER_SEC << endl;

    system("pause");
    return 0;
}

ベンチマーク結果:

編集:実際の Core 2アーキテクチャマシンでの結果:

Intel Xeon X5482 Harpertown @ 3.2 GHz x 2:

#define ALLOCATE_SEPERATE
#define ONE_LOOP
00600020
006D0020
007A0020
00870020
seconds = 6.206

#define ALLOCATE_SEPERATE
//#define ONE_LOOP
005E0020
006B0020
00780020
00850020
seconds = 2.116

//#define ALLOCATE_SEPERATE
#define ONE_LOOP
00570020
00633520
006F6A20
007B9F20
seconds = 1.894

//#define ALLOCATE_SEPERATE
//#define ONE_LOOP
008C0020
00983520
00A46A20
00B09F20
seconds = 1.993

観察:

  • 6.206秒 1つのループとし、2.116秒 2つのループを有します。これにより、OPの結果が正確に再現されます。

  • 最初の2つのテストでは、配列は個別に割り当てられます。これらはすべて、ページに対して同じ配置になっていることがわかります。

  • 2番目の2つのテストでは、配列がパックされ、その配置が崩れます。ここで、両方のループが高速であることがわかります。さらに、2番目の(ダブル)ループは、通常予想されるよりも遅いループになりました。

@Stephen Cannonがコメントで指摘しているように、この配置がロード/ストアユニットまたはキャッシュで誤ったエイリアシングを引き起こす可能性が非常に高いです。私はこれをグーグル検索して、Intelが実際に部分的なアドレスエイリアシングストールのためのハードウェアカウンターを持っていることを発見しました:

http://software.intel.com/sites/products/documentation/doclib/stdxe/2013/~amplifierxe/pmw_dp/events/partial_address_alias.html


5地域-説明

地域1:

これは簡単です。データセットは非常に小さいため、パフォーマンスはループや分岐などのオーバーヘッドによって支配されます。

地域2:

ここでは、データサイズが増加すると、相対的なオーバーヘッドの量が減少し、パフォーマンスが「飽和」します。ここでは、2つのループと分岐オーバーヘッドがあるため、2つのループの方が低速です。

ここで何が起こっているのか正確にはわかりません... Agner Fogがキャッシュバンクの競合について言及しているため、アライメントは依然として効果を発揮する可能性があります。(このリンクはSandy Bridgeに関するものですが、このアイデアはCore 2にも適用できるはずです。)

地域3:

この時点で、データはL1キャッシュに収まりません。したがって、パフォーマンスはL1 <-> L2キャッシュ帯域幅によって制限されます。

地域4:

シングルループでのパフォーマンスの低下が観察されています。そして、前述のように、これはプロセッサロード/ストアユニットで誤ったエイリアシングストールを引き起こす(ほとんどの場合)アライメントによるものです。

ただし、偽のエイリアシングが発生するためには、データセット間に十分なストライドが必要です。これが、リージョン3でこれが表示されない理由です。

地域5:

この時点では、何もキャッシュに収まりません。つまり、メモリ帯域幅に縛られます。


2 x Intel X5482 Harpertown @ 3.2 GHz Intel Core i7 870 @ 2.8 GHz Intel Core i7 2600K @ 4.4 GHz


162
+1:これが答えだと思います。他のすべての答えが言うこととは対照的に、それは本質的にキャッシュミスが多いシングルループバリアントではなく、キャッシュミスを引き起こしている配列の特定のアライメントに関するものです。
オリバーチャールズワース2011

30
この; 最も可能性が高いのは、偽のエイリアシングストールです。
スティーブンキャノン、

7
@VictorT。OPがリンクされているコードを使用しました。Excelで開いてグラフを作成できる.cssファイルを生成します。
Mysticial 2011

5
@Nawazページは通常4KBです。私が出力する16進数のアドレスを見ると、個別に割り当てられたテストはすべて4096を法として同じです(つまり、4KB境界の先頭から32バイトです)おそらくGCCにはこの動作がありません。これが、違いが見られない理由の説明になります。
Mysticial 2011


224

はい、正しい答えは間違いなくCPUキャッシュで何かをしなければなりません。ただし、特にデータがない場合、キャッシュ引数を使用するのは非常に困難です。

多くの議論につながった多くの答えがありますが、それに直面しましょう:キャッシュの問題は非常に複雑で、1次元ではありません。それらはデータのサイズに大きく依存しているので、私の質問は不公平でした。キャッシュグラフの非常に興味深い点であることが判明しました。

@Mysticialの回答は、多くの人々(私を含む)を確信させました。おそらくそれが事実に依存しているように思われたのはそれだけだったのですが、それは真実の「データポイント」の1つにすぎませんでした。

そのため、私は彼のテスト(継続的割り当てと個別割り当てを使用)と@James 'Answerのアドバイスを組み合わせました。

以下のグラフは、使用されている正確なシナリオとパラメータに応じて、ほとんどの回答、特に質問と回答に対するコメントの大部分が完全に間違っているか真であると考えられることを示しています。

最初の質問はn = 100.000であったことに注意してください。このポイント(偶然)は特別な動作を示します。

  1. 1ループと2ループのバージョン間で最大の差異があります(ほぼ3倍)

  2. これは、1ループ(つまり、連続割り当て)が2ループバージョンに勝る唯一のポイントです。(これにより、Mysticialの回答が可能になりました。)

初期化されたデータを使用した結果:

ここに画像の説明を入力してください

初期化されていないデータを使用した結果(これはMysticialがテストしたものです):

ここに画像の説明を入力してください

そしてこれは説明するのが難しいものです。初期化されたデータは、一度割り当てられ、異なるベクトルサイズの次のテストケースごとに再利用されます。

ここに画像の説明を入力してください

提案

キャッシュ関連のデータサイズ全体のMFLOPS情報を提供するには、スタックオーバーフローに関するパフォーマンスに関する低レベルの質問がすべて必要です。答えを考え、特にこの情報なしで他の人とそれらについて話し合うことは、みんなの時間の無駄です。


18
+1素晴らしい分析。そもそもデータを初期化しないままにするつもりはありませんでした。とにかく、たまたまアロケータがそれらをゼロにしたのです。したがって、初期化されたデータが重要です。実際の Core 2アーキテクチャマシンでの結果を使用して私の回答を編集しましたが、それらはあなたが観察しているものに非常に近いものです。もう一つは、私はサイズの範囲をテストしたことでn、それがために同じ性能のギャップを示してn = 80000, n = 100000, n = 200000...、など
Mysticial

2
@Mysticial私は、プロセス間のスパイの可能性を回避するために、プロセスに新しいページを与えるときはいつでも、OSがページゼロ化を実装すると思います。
v.oddou 2017年

1
@ v.oddou:動作はOSにも依存します。IIRC、Windowsには、バックグラウンドでゼロアウトされた解放されたページへのスレッドがあり、すでにゼロ化されたページから要求を満たせない場合、VirtualAlloc呼び出しは、要求を満たすのに十分なゼロになるまでブロックします。対照的に、Linuxはゼロページを必要なだけコピーオンライトとしてマップし、書き込み時に、新しいデータを書き込む前に新しいゼロを新しいページにコピーします。どちらの方法でも、ユーザーモードプロセスの観点からは、ページはゼロになりますが、初期化されていないメモリの最初の使用は、通常、WindowsよりもLinuxの方がコストが高くなります。
ShadowRanger

81

2番目のループでは、キャッシュアクティビティが大幅に少なくなるため、プロセッサがメモリの要求に対応しやすくなります。


1
2番目のバリアントはキャッシュミスが少ないと言っていますか?どうして?
オリバーチャールズワース2011

2
@Oli:第一の変形では、プロセッサは、時間にアクセスする4本のメモリラインを必要とするa[i]b[i]c[i]及びd[i]第二の変形例では、ちょうど2つ必要です。これにより、追加中にこれらの行を補充することがより実行可能になります。
パピー2011

4
ただし、配列がキャッシュ内で衝突しない限り、各バリアントは、メインメモリとの間で正確に同じ数の読み取りと書き込みを必要とします。したがって、結論は(これらの2つの配列は常に衝突していると思います)です。
Oliver Charlesworth 2011

3
私はついていません。命令ごと(つまりのインスタンスごとx += y)には、2つの読み取りと1つの書き込みがあります。これはどちらのバリアントにも当てはまります。したがって、キャッシュ<-> CPU帯域幅の要件は同じです。コンフリクト、キャッシュがない限り、< - > RAMの帯域幅要件も同じ..です
オリバーCharlesworth

2
stackoverflow.com/a/1742231/102916に記載されているように、Pentium Mのハードウェアプリフェッチは12の異なるフォワードストリームを追跡できます(そして、私は後のハードウェアが少なくとも同じように機能することを期待します)。ループ2はまだ4つのストリームしか読み取っていないので、その制限内に十分収まります。
Brooks Moses

50

n一度に2つのアレイをメモリに保持できるだけの適切な値であるマシンで作業しているが、ディスクキャッシュを介して利用可能なメモリの合計が4つすべてを保持するにはまだ十分であると想像してください。

シンプルなLIFOキャッシングポリシーを想定すると、このコードは次のようになります。

for(int j=0;j<n;j++){
    a[j] += b[j];
}
for(int j=0;j<n;j++){
    c[j] += d[j];
}

最初の原因となるabRAMにロードされ、その後、RAMに完全に働いていたこと。また、第2のループを開始し、cそしてdその後、RAMにディスクからロードされて操作されることになります。

他のループ

for(int j=0;j<n;j++){
    a[j] += b[j];
    c[j] += d[j];
}

2つの配列をページアウトし、他の2つをページインします ループを回るたびします。これは明らかにはるかに遅くなります。

テストではディスクキャッシュが表示されない可能性がありますが、他の形式のキャッシュの副作用が表示される可能性があります。


ここには少し混乱/誤解があるようですので、例を使って少し詳しく説明します。

たとえばn = 2、バイトを処理しているとします。私のシナリオでは、こうして、RAMは4バイトない、残りのメモリは大幅に遅くなります(アクセスが100倍になるなど)。

バイトがキャッシュにない場合のかなりおかしなキャッシングポリシーを想定すると、そこに配置して次のバイトも取得しますが、その間に次のようなシナリオが得られます。

  • for(int j=0;j<n;j++){
     a[j] += b[j];
    }
    for(int j=0;j<n;j++){
     c[j] += d[j];
    }
  • cache a[0]a[1]then b[0]そしてb[1]、set a[0] = a[0] + b[0]in cache-キャッシュに4バイトa[0], a[1]ありb[0], b[1]ます。コスト= 100 + 100。

  • a[1] = a[1] + b[1]キャッシュに設定します。コスト= 1 + 1。
  • 繰り返しcd
  • 総コスト= (100 + 100 + 1 + 1) * 2 = 404

  • for(int j=0;j<n;j++){
     a[j] += b[j];
     c[j] += d[j];
    }
  • cache a[0]a[1]then b[0]そしてb[1]、set a[0] = a[0] + b[0]in cache-キャッシュに4バイトa[0], a[1]ありb[0], b[1]ます。コスト= 100 + 100。

  • イジェクトa[0], a[1], b[0], b[1]キャッシュとキャッシュからc[0]c[1]、その後d[0]d[1]セットc[0] = c[0] + d[0]キャッシュインチ コスト= 100 + 100。
  • 私がどこへ向かっているのか、あなたは見始めているのではないかと思います。
  • 総コスト= (100 + 100 + 100 + 100) * 2 = 800

これは、古典的なキャッシュスラッシュシナリオです。


12
これは誤りです。配列の特定の要素を参照しても、配列全体がディスク(またはキャッシュされていないメモリ)からページインされることはありません。関連ページやキャッシュラインがページインされるのみ。
ブルックスモーゼ

1
@Brooks Moses-ここで起こっているように、配列全体を歩くと、そうなります。
OldCurmudgeon、2011

1
ええ、しかし、それはループ全体で毎回起こることではなく、操作全体で起こることです。2番目のフォームは「ループの周りで毎回2つの配列と他の2つのページをページアウトする」と主張し、それが私が反対していることです。配列全体のサイズに関係なく、このループの途中で、RAMは4つの配列のそれぞれからのページを保持し、ループが終了するまでページアウトされません。
Brooks Moses、

nがちょうど2つの配列をメモリに一度に保持できるようにするための適切な値である特定の場合、1つのループで4つの配列のすべての要素にアクセスすると、必ずスラッシングが発生します。
OldCurmudgeon 2011

1
なぜあなたは、全体では、ループ2ページを滞在しているa1b1、むしろそれらのそれぞれのちょうど最初のページよりも、最初の割り当てのために?(5バイトのページを想定しているので、ページはRAMの半分ですか?これはスケーリングだけではなく、実際のプロセッサとは完全に異なります。)
Brooks Moses

35

これは、コードが異なるためではなく、キャッシュのためです。RAMはCPUレジスタよりも遅く、キャッシュメモリはCPU内にあり、変数が変更されるたびにRAMに書き込むことを回避します。しかし、RAMはキャッシュが大きいため、キャッシュは大きくありません。そのため、RAMの一部のみをマップします。

最初のコードは、遠くのメモリアドレスを変更し、各ループでそれらを交互に使用するため、キャッシュを無効にする必要が継続的にあります。

2番目のコードは交互に行われません。隣接するアドレスに2回流れるだけです。これにより、すべてのジョブがキャッシュで完了し、2番目のループが開始された後にのみジョブが無効になります。


これにより、キャッシュが継続的に無効になるのはなぜですか?
オリバーチャールズワース2011

1
@OliCharlesworth:キャッシュを、連続する範囲のメモリアドレスのハードコピーと考えてください。それらの一部ではないアドレスにアクセスするふりをする場合は、キャッシュを再ロードする必要があります。また、キャッシュ内の何かが変更された場合、RAMに書き戻す必要があります。そうしないと、失われます。サンプルコードでは、100'000整数(400kBytes)の4つのベクトルが、L1キャッシュの容量(128または256K)よりも大きい可能性があります。
Emilio Garavaglia

5
このシナリオでは、キャッシュのサイズによる影響はありません。各配列要素は1回だけ使用され、その後は削除されても問題ありません。キャッシュサイズは、一時的な局所性がある場合(つまり、将来同じ要素を再利用する場合)にのみ重要です。
Oliver Charlesworth

2
@OliCharlesworth:キャッシュに新しい値をロードする必要があり、変更された値がすでにその中にある場合、最初にそれを書き留める必要があり、これにより書き込みが発生するまで待機します。
エミリオガラヴァリア

2
しかし、OPのコードの両方のバリアントでは、各値は正確に1回変更されます。各バリアントで同じ数の書き戻しを行います。
Oliver Charlesworth

22

ここで説明した結果は再現できません。

貧弱なベンチマークコードが原因であるかどうかはわかりませんが、次のコードを使用すると、私のマシンで2つのメソッドが互いに10%以内であり、通常、1つのループは2つよりもわずかに高速です。期待する。

配列サイズは、8つのループを使用して2 ^ 16から2 ^ 24の範囲でした。ソース配列を初期化するように注意して、+=割り当てがFPUにdoubleとして解釈されるメモリのガベージを追加するように要求しなかった

私はそのようなの割り当てを置くなど、さまざまなスキームで遊んb[j]d[j]InitToZero[j]使用しても、ループの内部で、かつ+= b[j] = 1および+= d[j] = 1、私はかなり一貫性のある結果を得ました。

ご想像のとおり、を使用したループの初期化bd内部でInitToZero[j]は、とへの割り当ての前に連続して行われるため、結合されたアプローチに利点がaありましたc、まだ10%以内。図を行きます。

ハードウェアは、第3世代Core i7を搭載したDell XPS 8500です。 @ 3.4 GHzおよび8 GBメモリを搭載したです。2 ^ 16から2 ^ 24の場合、8つのループを使用すると、累積時間はそれぞれ44.987と40.965でした。Visual C ++ 2010、完全に最適化。

PS:ループをゼロにカウントダウンするように変更しましたが、組み合わせた方法はわずかに高速でした。頭を掻いた。新しい配列のサイズとループ数に注意してください。

// MemBufferMystery.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include <iostream>
#include <cmath>
#include <string>
#include <time.h>

#define  dbl    double
#define  MAX_ARRAY_SZ    262145    //16777216    // AKA (2^24)
#define  STEP_SZ           1024    //   65536    // AKA (2^16)

int _tmain(int argc, _TCHAR* argv[]) {
    long i, j, ArraySz = 0,  LoopKnt = 1024;
    time_t start, Cumulative_Combined = 0, Cumulative_Separate = 0;
    dbl *a = NULL, *b = NULL, *c = NULL, *d = NULL, *InitToOnes = NULL;

    a = (dbl *)calloc( MAX_ARRAY_SZ, sizeof(dbl));
    b = (dbl *)calloc( MAX_ARRAY_SZ, sizeof(dbl));
    c = (dbl *)calloc( MAX_ARRAY_SZ, sizeof(dbl));
    d = (dbl *)calloc( MAX_ARRAY_SZ, sizeof(dbl));
    InitToOnes = (dbl *)calloc( MAX_ARRAY_SZ, sizeof(dbl));
    // Initialize array to 1.0 second.
    for(j = 0; j< MAX_ARRAY_SZ; j++) {
        InitToOnes[j] = 1.0;
    }

    // Increase size of arrays and time
    for(ArraySz = STEP_SZ; ArraySz<MAX_ARRAY_SZ; ArraySz += STEP_SZ) {
        a = (dbl *)realloc(a, ArraySz * sizeof(dbl));
        b = (dbl *)realloc(b, ArraySz * sizeof(dbl));
        c = (dbl *)realloc(c, ArraySz * sizeof(dbl));
        d = (dbl *)realloc(d, ArraySz * sizeof(dbl));
        // Outside the timing loop, initialize
        // b and d arrays to 1.0 sec for consistent += performance.
        memcpy((void *)b, (void *)InitToOnes, ArraySz * sizeof(dbl));
        memcpy((void *)d, (void *)InitToOnes, ArraySz * sizeof(dbl));

        start = clock();
        for(i = LoopKnt; i; i--) {
            for(j = ArraySz; j; j--) {
                a[j] += b[j];
                c[j] += d[j];
            }
        }
        Cumulative_Combined += (clock()-start);
        printf("\n %6i miliseconds for combined array sizes %i and %i loops",
                (int)(clock()-start), ArraySz, LoopKnt);
        start = clock();
        for(i = LoopKnt; i; i--) {
            for(j = ArraySz; j; j--) {
                a[j] += b[j];
            }
            for(j = ArraySz; j; j--) {
                c[j] += d[j];
            }
        }
        Cumulative_Separate += (clock()-start);
        printf("\n %6i miliseconds for separate array sizes %i and %i loops \n",
                (int)(clock()-start), ArraySz, LoopKnt);
    }
    printf("\n Cumulative combined array processing took %10.3f seconds",
            (dbl)(Cumulative_Combined/(dbl)CLOCKS_PER_SEC));
    printf("\n Cumulative seperate array processing took %10.3f seconds",
        (dbl)(Cumulative_Separate/(dbl)CLOCKS_PER_SEC));
    getchar();

    free(a); free(b); free(c); free(d); free(InitToOnes);
    return 0;
}

MFLOPSが適切な測定基準であると決定された理由がわかりません。アイデアはメモリアクセスに焦点を当てることでしたが、浮動小数点の計算時間を最小限に抑えるようにしました。に出発しましたが+=、理由はわかりません。

計算のない直接的な割り当ては、メモリアクセス時間のより明確なテストであり、ループカウントに関係なく均一なテストを作成します。会話の一部を見逃したかもしれませんが、もう一度考えてみる価値があります。プラスが割り当てから外されている場合、累積時間はそれぞれ31秒でほぼ同じです。


1
ここで述べるミスアラインメントのペナルティは、アラインメントされていない個々のロード/ストア(アラインメントされていないSSEロード/ストアを含む)の場合です。ただし、パフォーマンスは異なる配列の相対的な配置に影響されるため、ここでは当てはまりません。命令レベルでのミスアライメントはありません。すべてのロード/ストアが適切に調整されます。
ミスティック

18

これは、CPUにそれほど多くのキャッシュミスがないためです(RAMチップからアレイデータが送信されるのを待つ必要があります)。配列のサイズを継続的に調整して、、CPUレベル1キャッシュ(L1)、次にレベル2キャッシュ(L2)し、コードにかかる時間をプロット配列のサイズに対して実行します。グラフは、予想したような直線であってはなりません。


2
キャッシュサイズと配列サイズの間に相互作用があるとは思いません。各配列要素は1回だけ使用され、その後安全に削除できます。ただし、4つの配列が競合する場合は、キャッシュラインサイズと配列サイズの間に相互作用がある可能性があります。
Oliver Charlesworth

15

最初のループは、各変数への書き込みを交互に行います。2番目と3番目のものは、要素サイズの小さなジャンプのみを行います。

20 cm離れたペンと紙で、20の十字の2本の平行線を書いてみてください。片方のラインを完成させてからもう一方のラインを試し、各ラインに交互に十字を書いて別の時間を試します。


CPU命令のようなものについて考えるとき、現実世界の活動の類推は危険に満ちています。説明しているのは、実質的にシーク時間です。これは、回転ディスクに保存されているデータの読み取り/書き込みについて話している場合に当てはまりますが、CPUキャッシュ(またはRAMやSSD)にはシーク時間はありません。隣接していないメモリ領域へのアクセスは、隣接するアクセスと比較してペナルティが発生しません。
FeRD

7

元の質問

1つのループが2つのループよりもはるかに遅いのはなぜですか?


結論:

事例1は、たまたま非効率的な問題である古典的な補間問題です。また、これが、多くのマシンアーキテクチャと開発者が、マルチスレッドアプリケーションや並列プログラミングを実行できるマルチコアシステムの構築と設計を行った主な理由の1つでもあると思います。

ハードウェア、OS、およびコンパイラーが連携して、RAM、キャッシュ、ページファイルなどの操作を含むヒープ割り当てを実行する方法を含まない、この種のアプローチからそれを見る。これらのアルゴリズムの基礎となる数学は、これらの2つのうちどちらが優れたソリューションであるかを示しています。

私たちは、のアナロジーを使用することができますBossされてSummation表現することFor Loop、労働者の間で移動しなければならないことAとしますB

移動に必要な距離と作業員間の所要時間の違いにより、ケース2ケース1より少しでも少なくても少なくとも半分の速度であることが簡単にわかります。この数学は、ほぼ仮想的に完全に、BenchMark Timesだけでなく、組立説明書の違いの数とも一致しています。


次に、これらすべてがどのように機能するかを説明します。


問題の評価

OPのコード:

const int n=100000;

for(int j=0;j<n;j++){
    a1[j] += b1[j];
    c1[j] += d1[j];
}

そして

for(int j=0;j<n;j++){
    a1[j] += b1[j];
}
for(int j=0;j<n;j++){
    c1[j] += d1[j];
}

考察

forループの2つのバリアントに関するOPの元の質問と、キャッシュの動作に関する修正された質問、およびその他の優れた回答と便利なコメントの多くを検討してください。この状況と問題について別のアプローチを取ることで、ここで別のことを試してみたいと思います。


アプローチ

2つのループと、キャッシュとページのファイリングに関するすべての説明を考慮すると、これを別の視点から見るために別のアプローチを採用したいと思います。キャッシュファイルやページファイル、メモリ割り当ての実行を含まないもの、実際には、このアプローチは実際のハードウェアやソフトウェアにはまったく関係ありません。


展望

しばらくコードを見ると、問題が何で、何がそれを生成しているのかが非常に明らかになりました。これをアルゴリズムの問​​題に分解し、数学的表記を使用する観点から見て、算術問題とアルゴリズムに類推を適用してみましょう。


私たちが知っていること

このループが100,000回実行されることはわかっています。我々はまた、それを知っていますa1b1c1d1 64ビットアーキテクチャ上のポインタです。32ビットマシンのC ++内では、すべてのポインターは4バイトであり、64ビットマシンでは、ポインターは固定長であるため、サイズは8バイトです。

どちらの場合も32バイトを割り当てる必要があることがわかっています。唯一の違いは、各反復で32バイトまたは2〜8バイトの2セットを割り当てることです。2番目のケースでは、両方の独立したループの反復ごとに16バイトを割り当てます。

両方のループは、合計割り当てで32バイトに等しくなります。この情報を使用して、次に進み、これらの概念の一般的な数学、アルゴリズム、および類似性を示します。

両方のケースで同じセットまたは操作のグループを実行する必要がある回数はわかっています。どちらの場合でも割り当てる必要のあるメモリの量はわかっています。両方のケース間の割り当ての全体的なワークロードはほぼ同じになると評価できます。


私たちが知らないこと

カウンターを設定してベンチマークテストを実行しない限り、各ケースにかかる時間はわかりません。ただし、ベンチマークは元の質問と回答およびコメントの一部からすでに含まれています。この2つの間に大きな違いがあることがわかります。これが、この問題に対するこの提案の根拠です。


調べよう

ヒープ割り当て、ベンチマークテスト、RAM、キャッシュ、およびページファイルを確認することで、多くの人がすでにこれを行っていることは明らかです。特定のデータポイントと特定の反復インデックスを確認することも含まれており、この特定の問題に関するさまざまな会話から、多くの人が他の関連する問題に疑問を持ち始めています。数学的アルゴリズムを使用して、これに類推を適用することによって、この問題をどのように検討し始めますか?まず、いくつかのアサーションを作成します。次に、そこからアルゴリズムを構築します。


私たちの主張:

  • ループとその反復は、ループでのように0で開始するのではなく、1で始まり100000で終了する合計であるようにします。アルゴリズム自体。
  • どちらの場合も、処理する4つの関数と2つの関数呼び出しがあり、各関数呼び出しで2つの操作が実行されます。私たちは、次のような機能への機能や通話など、これらのセットアップを設定します:F1()F2()f(a)f(b)f(c)f(d)

アルゴリズム:

1番目のケース: -合計は1つだけですが、2つの独立した関数呼び出し。

Sum n=1 : [1,100000] = F1(), F2();
                       F1() = { f(a) = f(a) + f(b); }
                       F2() = { f(c) = f(c) + f(d); }

2番目のケース: -2つの合計ですが、それぞれに独自の関数呼び出しがあります。

Sum1 n=1 : [1,100000] = F1();
                        F1() = { f(a) = f(a) + f(b); }

Sum2 n=1 : [1,100000] = F1();
                        F1() = { f(c) = f(c) + f(d); }

あなたが気づいた場合F2()のみに存在するSumから、Case1どこF1()に含まれているSumからCase1との両方にSum1してSum2からCase2。これは、2番目のアルゴリズム内で最適化が行われていると結論付け始めた後で明らかになります。

最初のケースのSum呼び出しによる反復f(a)では、自己に追加され、f(b)次にf(c)同じように実行さf(d)れますが、100000反復ごとに追加されます。後者の場合、我々は持っているSum1Sum2、彼らは2回連続で呼び出されている同じ機能であるかのように、両方同じに作用します。

このケースでは扱うことができますSum1し、Sum2単に昔ながらとしてSumどこSumこの場合、このようなルックスで:Sum n=1 : [1,100000] { f(a) = f(a) + f(b); }今、私たちはただ、それは同じ機能であることを考慮することができ、最適化のようなこのルックス。


類推のまとめ

2番目のケースで見たものでは、両方のforループがまったく同じシグネチャを持っているため、最適化が行われているように見えますが、これは本当の問題ではありません。問題は、によって行われている作業ではありませんf(a)f(b)f(c)、とf(d)。どちらの場合も、2つを比較した場合も、実行時間に違いが出るのは、Summationがそれぞれの場合に移動しなければならない距離の違いです。

考えてFor LoopsいるようSummationsであるとして反復を行い、そのBoss二人に命令を与えていることAB、そのジョブが肉にしていることCD、それぞれ、そこからいくつかのパッケージをピックアップし、それを返すように。この類推では、forループまたは合計の反復と条件チェック自体は実際にはを表していませんBoss。何実際に表すことBoss直接実際の数学的アルゴリズムからではなく、実際の概念からScope及びCode Block等ルーチンまたはサブルーチン内の、方法、機能、翻訳部、最初のアルゴリズムは、第2のアルゴリズムは、2つの連続範囲を有する1つの範囲を有します。

各コールスリップの最初のケース内で、はにBoss移動しAて注文を出し、パッケージAをフェッチするためにオフにB'sなり、次ににBoss移動Cして同じことを行い、D各反復でパッケージを受け取るために注文を出します。

2番目のケースでは、すべてのパッケージが受信されるまで、BossAtoと直接連携してB'sパッケージをフェッチします。次に、はとBoss連携しCて、すべてのD'sパッケージを取得するために同じことを行います。

8バイトのポインターを使用してヒープ割り当てを処理しているので、次の問題を考えてみましょう。がBossから100フィートAAが500フィートであるとしましょうC。実行の順序のため、Boss最初からどれくらい離れているかについて心配する必要はありませんC。どちらの場合も、Boss最初は最初からA次にに移動しBます。このアナロジーは、この距離が正確であると言っているのではありません。これは、アルゴリズムの動作を示すための便利なテストケースシナリオです。

ヒープの割り当てを行い、キャッシュファイルとページファイルを操作する場合、多くの場合、アドレスの場所間のこれらの距離はそれほど変わらないか、データ型と配列サイズの性質によって大幅に変わる可能性があります。


テストケース:

最初のケースは:最初の反復ではBoss、当初に注文票を与えるために100フィートを行かなければならないAA消灯し、彼のことをしますが、その後Bossする500フィートを移動しなければならないC彼に彼の注文票を得ました。次に、次の反復とその後の1回おきの反復Bossで、2つの間を500フィート往復します。

後者の場合は:Bossへの最初の反復で100フィートを移動しなければならないAが、その後、彼はすでに存在しているとのためにちょうど待ってA、すべての伝票が満たされるまで戻って取得します。その後Bossの最初の反復で500フィートを移動しなければならないCので、C500フィートからですA。これBoss( Summation, For Loop )は、作業の直後に呼び出されているため、注文伝票がすべて完了するまでA、彼と同じように待機します。 AC's


走行距離の違い

const n = 100000
distTraveledOfFirst = (100 + 500) + ((n-1)*(500 + 500); 
// Simplify
distTraveledOfFirst = 600 + (99999*100);
distTraveledOfFirst = 600 + 9999900;
distTraveledOfFirst =  10000500;
// Distance Traveled On First Algorithm = 10,000,500ft

distTraveledOfSecond = 100 + 500 = 600;
// Distance Traveled On Second Algorithm = 600ft;    

任意の値の比較

600は1000万をはるかに下回っていることは容易にわかります。今、これは正確ではありません。なぜなら、RAMのどのアドレス間、または各反復での各呼び出しからのキャッシュまたはページファイル間の距離の実際の違いは、他の多くの目に見えない変数が原因であるからです。これは、認識して最悪のシナリオから見た状況の評価にすぎません。

これらの数値から、アルゴリズム1は99%アルゴリズム2よりも低速であるかのように見えます。しかし、これが唯一であるBoss's一部またはアルゴリズムの責任と、それが実際の労働者を考慮していないABC、&Dと彼らはそれぞれ、ループの反復ごとに行う必要があります。したがって、上司の仕事は、行われている作業全体の約15〜40%しか占めていません。ワーカーを介して行われる作業の大部分は、速度速度の差の比率を約50〜70%に維持することに少し大きな影響を与えます。


観察: - 2つのアルゴリズムの間の違い

この状況では、行われている作業のプロセスの構造です。ケース2は、名前と移動距離が異なる変数のみである、同様の関数宣言と定義の部分的な最適化の両方からより効率的であることを示しています。

また、ケース1で移動した合計距離はケース2で移動した距離よりもはるかに遠く、2つのアルゴリズム間でこの時間距離を移動した距離と見なすことができます。ケース1には、ケース2よりもかなり多くの作業が必要です。

これはASM、両方のケースで示された指示の証拠から観察できます。これらのケースについてすでに述べたことに加えて、これはケース1でボスがそれぞれのイテレーションで再び戻る前に両方ACを待つ必要があるという事実を考慮していませんA。また、非常に長い時間がかかる場合、Aまたは他のワーカーが実行を待機してアイドル状態にあるという事実は考慮されていません。BBoss

ケース2つのみビーイングアイドルがあるBoss労働者が取り戻すまで。したがって、これもアルゴリズムに影響を与えます。



OPは質問を修正しました

編集:動作は配列(n)とCPUキャッシュのサイズに大きく依存するため、問題は無関係であることが判明しました。したがって、さらに関心がある場合は、質問を言い換えます。

次のグラフの5つの領域で示されているように、さまざまなキャッシュ動作につながる詳細への確かな洞察を提供できますか?

これらのCPUについて同様のグラフを提供することにより、CPU /キャッシュアーキテクチャの違いを指摘することも興味深いかもしれません。


これらの質問について

私が疑いなく示したように、ハードウェアとソフトウェアが関与する前でさえ、根本的な問題があります。

ここで、メモリの管理やページファイルなどのキャッシュの管理については、以下のシステムの統合されたセットですべて一緒に機能します。

  • The Architecture {ハードウェア、ファームウェア、一部の組み込みドライバー、カーネル、およびASM命令セット}。
  • The OS{ファイルおよびメモリ管理システム、ドライバー、レジストリ}。
  • The Compiler {ソースコードの翻訳単位と最適化}。
  • そしてSource Code、独特のアルゴリズムのセットを備えたそれ自体さえ。

我々はすでに私たちも任意で任意のマシンにそれを適用する前に、最初のアルゴリズムの中に起こっているボトルネックがあることがわかりますArchitectureOSと、Programmable Language第2のアルゴリズムに比べて。現代のコンピュータの本質を巻き込む前に、すでに問題が存在していました。


エンディング結果

しかしながら; これらの新しい質問は、それら自体が重要であり、結局のところ役割を果たすので、重要ではないと言っているのではありません。これらは手順と全体的なパフォーマンスに影響を与えます。これは、回答やコメントを提供した多くの人からのさまざまなグラフと評価から明らかです。

あなたがのアナロジーに注意を払った場合Bossと2人の労働者AB行くとからパッケージを取得しなければならなかったCD、それぞれ、問題の2つのアルゴリズムの数学的表記を考慮。コンピュータのハードウェアとソフトウェアの関与がなくて、あなたが見ることができるCase 2程度であり、60%より速いですCase 1

これらのアルゴリズムをいくつかのソースコードに適用し、コンパイル、最適化、OSを介して実行して特定のハードウェア上で操作を実行した後、グラフとチャートを見ると、違いの間にもう少し劣化があることがわかりますこれらのアルゴリズムで。

Dataセットがかなり小さい場合、最初はそれほど悪い違いとは思えないかもしれません。しかし、以降はCase 1約ある60 - 70%より遅くCase 2、我々は時間の実行の違いという点で、この関数の成長を見ることができます。

DeltaTimeDifference approximately = Loop1(time) - Loop2(time)
//where 
Loop1(time) = Loop2(time) + (Loop2(time)*[0.6,0.7]) // approximately
// So when we substitute this back into the difference equation we end up with 
DeltaTimeDifference approximately = (Loop2(time) + (Loop2(time)*[0.6,0.7])) - Loop2(time)
// And finally we can simplify this to
DeltaTimeDifference approximately = [0.6,0.7]*Loop2(time)

この近似は、アルゴリズムによるこれらの2つのループと、ソフトウェアの最適化や機械命令を含む機械操作の平均差です。

データセットが線形に増加すると、2つの間の時間差も増加します。アルゴリズム1は、場合明らかであるアルゴリズム2以上フェッチを有するBoss走行前後の間の最大距離を有しているACアルゴリズム2はながら最初の反復の後にすべての反復のためBossに移動しなければならないA一度、その後で行われた後A、彼は旅行に持っていますからに移動Aするときの最大距離は1回だけCです。

Boss同じような連続したタスクに集中するのではなく、2つの類似したことを一度に実行して前後にジャグリングすることに集中しようとすると、彼は2倍旅行と仕事をしなければならなかったので、1日の終わりまでにかなり怒ります。したがって、上司の配偶者や子供はそれを高く評価しないので、上司が補間されたボトルネックに入るようにして、状況の範囲を失わないでください。



修正:ソフトウェアエンジニアリングの設計原則

-差Local StackHeap Allocated計算繰り返し内ループおよびそれらの用途、それらの効率および有効性の差-

上記で提案した数学的アルゴリズムは、ヒープに割り当てられたデータに対して演算を実行するループに主に適用されます。

  • 連続したスタック操作:
    • ループがスタックフレーム内の単一のコードブロックまたはスコープ内でローカルにデータの操作を実行している場合でも、ループは適用されますが、メモリの場所は、通常は連続的であり、移動距離または実行時間の違いによりはるかに近くなりますほとんど無視できます。ヒープ内で割り当てが行われていないため、メモリが分散されず、メモリがRAM経由でフェッチされていません。メモリは通常、シーケンシャルであり、スタックフレームとスタックポインタに対して相対的です。
    • スタックで連続した操作が行われている場合、最新のプロセッサは反復的な値とアドレスをキャッシュし、これらの値をローカルキャッシュレジスタ内に保持します。ここでの操作または指示の時間は、ナノ秒のオーダーです。
  • 連続したヒープ割り当て操作:
    • ヒープ割り当ての適用を開始し、CPUのアーキテクチャ、バスコントローラー、およびRAMモジュールに応じて、プロセッサーが連続した呼び出しでメモリアドレスをフェッチする必要がある場合、操作または実行の時間はマイクロからミリ秒。キャッシュされたスタック操作と比較して、これらはかなり遅いです。
    • CPUはRAMからメモリアドレスをフェッチする必要があり、通常、システムバス全体でCPU自体の内部データパスまたはデータバスに比べて低速です。

したがって、ヒープ上にある必要のあるデータを処理していて、それらをループでトラバースする場合は、各データセットとそれに対応するアルゴリズムを単一のループ内に保持する方が効率的です。ヒープ上にある異なるデータセットの複数の操作を1つのループに入れることにより、連続するループを除外するよりも優れた最適化が得られます。

スタック上にあるデータは頻繁にキャッシュされるため、これを行うことは問題ありませんが、反復ごとにメモリアドレスを照会する必要があるデータについてはできません。

ここで、ソフトウェアエンジニアリングとソフトウェアアーキテクチャデザインが役立ちます。これは、データを整理する方法、データをキャッシュするタイミング、ヒープにデータを割り当てるタイミング、アルゴリズムの設計と実装の方法、およびいつどこで呼び出すかを理解する能力です。

同じデータセットに関連する同じアルゴリズムがあるかもしれませんが、O(n)作業時のアルゴリズムの複雑さから見られる上記の問題のために、スタックバリアント用の実装設計とヒープ割り当てバリアント用の別の実装設計が必要になる場合があります。ヒープで。

私が長年にわたって気づいたことから、多くの人々はこの事実を考慮に入れていません。彼らは、特定のデータセットで機能する1つのアルゴリズムを設計する傾向があり、スタックでローカルにキャッシュされているデータセットに関係なく、またはヒープに割り当てられているかどうかに関係なく、そのアルゴリズムを使用します。

真の最適化が必要な場合は、コードの重複のように見えるかもしれませんが、一般化するには、同じアルゴリズムの2つのバリアントを使用する方が効率的です。1つはスタック操作用、もう1つは反復ループで実行されるヒープ操作用です。

これが疑似例です:2つの単純な構造体、1つのアルゴリズム。

struct A {
    int data;
    A() : data{0}{}
    A(int a) : data{a}{} 
};
struct B {
    int data;
    B() : data{0}{}
    A(int b) : data{b}{}
}                

template<typename T>
void Foo( T& t ) {
    // do something with t
}

// some looping operation: first stack then heap.

// stack data:
A dataSetA[10] = {};
B dataSetB[10] = {};

// For stack operations this is okay and efficient
for (int i = 0; i < 10; i++ ) {
   Foo(dataSetA[i]);
   Foo(dataSetB[i]);
}

// If the above two were on the heap then performing
// the same algorithm to both within the same loop
// will create that bottleneck
A* dataSetA = new [] A();
B* dataSetB = new [] B();
for ( int i = 0; i < 10; i++ ) {
    Foo(dataSetA[i]); // dataSetA is on the heap here
    Foo(dataSetB[i]); // dataSetB is on the heap here
} // this will be inefficient.

// To improve the efficiency above, put them into separate loops... 

for (int i = 0; i < 10; i++ ) {
    Foo(dataSetA[i]);
}
for (int i = 0; i < 10; i++ ) {
    Foo(dataSetB[i]);
}
// This will be much more efficient than above.
// The code isn't perfect syntax, it's only psuedo code
// to illustrate a point.

これは、スタックバリアントとヒープバリアントの個別の実装を使用することで参照していたものです。アルゴリズム自体はそれほど重要ではありません。その中で使用するのはループ構造です。


私がこの回答を投稿してからしばらく時間が経過しましたが、これも理解するのに役立つ可能性のある簡単なコメントを追加したいと思います。このボスは、スコープとスタック変数を管理するスタックフレームとスタックポインターとforループのメモリアドレス指定の組み合わせであると考えてください。
Francis Cugler 2017年

@PeterMortensen元の回答を少し変更して、アドバイスを考慮に入れました。これはあなたが提案していたことだと思います。
Francis Cugler、

2

古いC ++と最適化の可能性があります。私のコンピューターでは、ほぼ同じ速度が得られました。

1ループ:1.577 ms

2つのループ:1.507 ms

16 GBのRAMを搭載したE5-1620 3.5 GHzプロセッサでVisual Studio 2015を実行しています。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.