帯域幅とキャッシュ・アフィニティー

単純な関数 Foo では、並列ループで記述された場合に大幅な速度向上が見込めません。原因は、プロセッサーとメモリー間のシステム帯域幅が不十分なためです。この場合、キャッシュを活用することを考えてアルゴリズムを検討する必要があります。キャッシュの利用を考慮した構造の見直しは、シリアルプログラムはもちろん、並列プログラムでも有益です。

一部のケースでは作業を再構成する代わりに affinity_partitioner を使用します。このパーティショナーは、粒度を自動的に選択するだけでなく、キャッシュ・アフィニティーを最適化して、スレッド間のデータを均一に分配しようとします。affinity_partitioner を使用すると、次の場合にパフォーマンスを大幅に向上できます。

次のコードは、affinity_partitioner の使用方法を示しています。

#include "tbb/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 をローカル・スタティック・オブジェクトとして宣言することにより、パーティショナーの寿命を取得しています。別のアプローチは、TimeStepFoo の反復ループの外でアフィニティーを宣言して、parallel_for に渡す方法です。

データがシステムのキャッシュに格納できない場合、利点はほとんどありません。次の図は、この状況を示しています。

データセットとキャッシュの相対サイズにより決定されたアフィニティーの利点

次の図は、並列処理の高速化がデータセットのサイズに応じてどのように変化するかを示しています。サンプルの計算は、A[i]+=B[i] で、i の範囲は [0,N) です。この条件は、大きな変化を示すために選択されたものです。通常のコードではこのように大きな変化は起こりません。グラフの両端はあまり高速化されていません。N が小さい場合、並列スケジュールのオーバーヘッドが大きくなるため、あまり高速化されません。N が大きい場合、データセットが大きくなるため、ループ間でキャッシュに格納して処理できません。中間のピークがアフィニティーのスイートスポットです。したがって、メモリーアクセスに対する計算の比率が低い場合は、affinity_partitioner を万能薬ではなく単なるツールと考えるべきです。

アフィニティーによる向上は配列サイズに依存

最適化に関する注意事項

インテル® コンパイラーは、互換マイクロプロセッサー向けには、インテル製マイクロプロセッサー向けと同等レベルの最適化が行われない可能性があります。これには、インテル® ストリーミング SIMD 拡張命令 2 (インテル® SSE2)、インテル® ストリーミング SIMD 拡張命令 3 (インテル® SSE3)、ストリーミング SIMD 拡張命令 3 補足命令 (SSSE3) 命令セットに関連する最適化およびその他の最適化が含まれます。インテルでは、インテル製ではないマイクロプロセッサーに対して、最適化の提供、機能、効果を保証していません。本製品のマイクロプロセッサー固有の最適化は、インテル製マイクロプロセッサーでの使用を目的としています。インテル® マイクロアーキテクチャーに非固有の特定の最適化は、インテル製マイクロプロセッサー向けに予約されています。この注意事項の適用対象である特定の命令セットの詳細は、該当する製品のユーザー・リファレンス・ガイドを参照してください。

改訂 #20110804