元の質問
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回実行されることはわかっています。我々はまた、それを知っていますa1
、b1
、c1
&d1
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
反復ごとに追加されます。後者の場合、我々は持っているSum1
とSum2
、彼らは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
二人に命令を与えていることA
とB
、そのジョブが肉にしていることC
とD
、それぞれ、そこからいくつかのパッケージをピックアップし、それを返すように。この類推では、forループまたは合計の反復と条件チェック自体は実際にはを表していませんBoss
。何実際に表すことBoss
直接実際の数学的アルゴリズムからではなく、実際の概念からScope
及びCode Block
等ルーチンまたはサブルーチン内の、方法、機能、翻訳部、最初のアルゴリズムは、第2のアルゴリズムは、2つの連続範囲を有する1つの範囲を有します。
各コールスリップの最初のケース内で、はにBoss
移動しA
て注文を出し、パッケージA
をフェッチするためにオフにB's
なり、次ににBoss
移動C
して同じことを行い、D
各反復でパッケージを受け取るために注文を出します。
2番目のケースでは、すべてのパッケージが受信されるまで、Boss
はA
toと直接連携してB's
パッケージをフェッチします。次に、はとBoss
連携しC
て、すべてのD's
パッケージを取得するために同じことを行います。
8バイトのポインターを使用してヒープ割り当てを処理しているので、次の問題を考えてみましょう。がBoss
から100フィートA
、A
が500フィートであるとしましょうC
。実行の順序のため、Boss
最初からどれくらい離れているかについて心配する必要はありませんC
。どちらの場合も、Boss
最初は最初からA
次にに移動しB
ます。このアナロジーは、この距離が正確であると言っているのではありません。これは、アルゴリズムの動作を示すための便利なテストケースシナリオです。
ヒープの割り当てを行い、キャッシュファイルとページファイルを操作する場合、多くの場合、アドレスの場所間のこれらの距離はそれほど変わらないか、データ型と配列サイズの性質によって大幅に変わる可能性があります。
テストケース:
最初のケースは:最初の反復ではBoss
、当初に注文票を与えるために100フィートを行かなければならないA
とA
消灯し、彼のことをしますが、その後Boss
する500フィートを移動しなければならないC
彼に彼の注文票を得ました。次に、次の反復とその後の1回おきの反復Boss
で、2つの間を500フィート往復します。
後者の場合は:Boss
への最初の反復で100フィートを移動しなければならないA
が、その後、彼はすでに存在しているとのためにちょうど待ってA
、すべての伝票が満たされるまで戻って取得します。その後Boss
の最初の反復で500フィートを移動しなければならないC
ので、C
500フィートからですA
。これBoss( Summation, For Loop )
は、作業の直後に呼び出されているため、注文伝票がすべて完了するまでA
、彼と同じように待機します。 A
C'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
一部またはアルゴリズムの責任と、それが実際の労働者を考慮していないA
、B
、C
、&D
と彼らはそれぞれ、ループの反復ごとに行う必要があります。したがって、上司の仕事は、行われている作業全体の約15〜40%しか占めていません。ワーカーを介して行われる作業の大部分は、速度速度の差の比率を約50〜70%に維持することに少し大きな影響を与えます。
観察: - 2つのアルゴリズムの間の違い
この状況では、行われている作業のプロセスの構造です。ケース2は、名前と移動距離が異なる変数のみである、同様の関数宣言と定義の部分的な最適化の両方からより効率的であることを示しています。
また、ケース1で移動した合計距離はケース2で移動した距離よりもはるかに遠く、2つのアルゴリズム間でこの時間距離を移動した距離と見なすことができます。ケース1には、ケース2よりもかなり多くの作業が必要です。
これはASM
、両方のケースで示された指示の証拠から観察できます。これらのケースについてすでに述べたことに加えて、これはケース1でボスがそれぞれのイテレーションで再び戻る前に両方A
とC
を待つ必要があるという事実を考慮していませんA
。また、非常に長い時間がかかる場合、A
または他のワーカーが実行を待機してアイドル状態にあるという事実は考慮されていません。B
Boss
でケース2つのみビーイングアイドルがあるBoss
労働者が取り戻すまで。したがって、これもアルゴリズムに影響を与えます。
OPは質問を修正しました
編集:動作は配列(n)とCPUキャッシュのサイズに大きく依存するため、問題は無関係であることが判明しました。したがって、さらに関心がある場合は、質問を言い換えます。
次のグラフの5つの領域で示されているように、さまざまなキャッシュ動作につながる詳細への確かな洞察を提供できますか?
これらのCPUについて同様のグラフを提供することにより、CPU /キャッシュアーキテクチャの違いを指摘することも興味深いかもしれません。
これらの質問について
私が疑いなく示したように、ハードウェアとソフトウェアが関与する前でさえ、根本的な問題があります。
ここで、メモリの管理やページファイルなどのキャッシュの管理については、以下のシステムの統合されたセットですべて一緒に機能します。
The Architecture
{ハードウェア、ファームウェア、一部の組み込みドライバー、カーネル、およびASM命令セット}。
The OS
{ファイルおよびメモリ管理システム、ドライバー、レジストリ}。
The Compiler
{ソースコードの翻訳単位と最適化}。
- そして
Source Code
、独特のアルゴリズムのセットを備えたそれ自体さえ。
我々はすでに私たちも任意で任意のマシンにそれを適用する前に、最初のアルゴリズムの中に起こっているボトルネックがあることがわかりますArchitecture
、OS
と、Programmable Language
第2のアルゴリズムに比べて。現代のコンピュータの本質を巻き込む前に、すでに問題が存在していました。
エンディング結果
しかしながら; これらの新しい質問は、それら自体が重要であり、結局のところ役割を果たすので、重要ではないと言っているのではありません。これらは手順と全体的なパフォーマンスに影響を与えます。これは、回答やコメントを提供した多くの人からのさまざまなグラフと評価から明らかです。
あなたがのアナロジーに注意を払った場合Boss
と2人の労働者A
&B
行くとからパッケージを取得しなければならなかったC
とD
、それぞれ、問題の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
走行前後の間の最大距離を有しているA
とC
アルゴリズム2はながら最初の反復の後にすべての反復のためBoss
に移動しなければならないA
一度、その後で行われた後A
、彼は旅行に持っていますからに移動A
するときの最大距離は1回だけC
です。
Boss
同じような連続したタスクに集中するのではなく、2つの類似したことを一度に実行して前後にジャグリングすることに集中しようとすると、彼は2倍旅行と仕事をしなければならなかったので、1日の終わりまでにかなり怒ります。したがって、上司の配偶者や子供はそれを高く評価しないので、上司が補間されたボトルネックに入るようにして、状況の範囲を失わないでください。
修正:ソフトウェアエンジニアリングの設計原則
-差Local Stack
とHeap 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.
これは、スタックバリアントとヒープバリアントの個別の実装を使用することで参照していたものです。アルゴリズム自体はそれほど重要ではありません。その中で使用するのはループ構造です。