帯域幅とキャッシュ・アフィニティー#
単純な関数 Foo では、並列ループで記述されてもスピードアップが見込めない可能性があります。原因は、プロセッサーとメモリー間のシステムバス帯域幅が不十分なためです。その場合、キャッシュをより有効に活用するためアルゴリズムを再考する必要があるかもしれません。キャッシュをより有効に活用する再構築は、通常、シリアルプログラムだけでなく並列プログラムでもメリットを得られます。
再構築の代替として、一部のケースで有効な方法が affinity_partitioner です。これは粒度を自動的に選択するだけでなく、キャッシュ・アフィニティーを最適化し、データをスレッド間で均一に分散しようとします。次の場合に、affinity_partitioner を使用するとパフォーマンスが大幅に向上します。
計算では、データアクセスごとにいくつかの操作が行われます。
ループで処理されるデータはキャッシュに収まります。
ループ、または同様のループが同じデータに対して再実行されます。
2 つ以上のハードウェア・スレッドが利用可能です (特に、スレッド数が 2 の累乗でない場合)。使用できるスレッドが 2 つしかない場合、通常、oneAPI スレッディング・ビルディング・ブロック (oneTBB) のデフォルト・スケジュールで十分なキャッシュ・アフィニティーをもたらします。
次のコードは、affinity_partitioner を使用する方法を示しています。
#include "oneapi/tbb.h"
void ParallelApplyFoo( float a[], size_t n ) {
static affinity_partitioner ap;
parallel_for(blocked_range<size_t>(0,n), ApplyFoo(a), ap);
}
void TimeStepFoo( float a[], size_t n, int steps ) {
for( int t=0; t<steps; ++t )
ParallelApplyFoo( a, n );
}この例では、affinity_partitioner オブジェクト ap はループの反復間に存在します。ループ反復が実行された場所を記憶し、各反復を、それを以前に実行した同じスレッドに渡すことができるようになります。サンプルコードでは、affinity_partitioner をローカルな静的オブジェクトとして宣言することで、パーティショナーのライフタイムを正しく設定します。もう 1 つの方法として、TimeStepFoo の反復ループの外側のスコープで宣言し、呼び出しチェーンを通じて parallel_for に渡す方法があります。
データがシステムのキャッシュに収まらないと、ほとんどメリットが得られない可能性があります。以下の図に、その状況を示します。
次の図は、データセットのサイズによって並列化のスケジュールがどのように変化するか示しています。この例の計算は、i が [0,N) のレンジ内にある場合、A[i]+=B[i] となります。それは劇的な効果を得るために選ばれました。皆さんのコードでは、これほどまでの変化は見られないしょう。グラフを見ると、極端な改善は見られません。N が小さい場合、並列スケジュールのオーバーヘッドが大幅に影響し、スピードアップはほとんど得られません。N が大きい場合、データセットはループ呼び出し間でキャッシュに格納し続けるには大きすぎます。真ん中のピークがアフィニティーのスイートスポットです。したがって、メモリーアクセスに対する計算の比率が低い場合、affinity_partitioner は万全ではなくツールとして考える必要があります。
製品および性能に関する情報
性能は、使用状況、構成、およびその他の要因によって異なります。詳細については、www.intel.com/PerformanceIndex (英語) をご覧ください。

