すべてのマルチタスク OS には、プロセス スケジューラと呼ばれるものがあります。これは、各プロセスをいつどこで実行するかを決定する OS コンポーネントです。スケジューラは通常、意思決定に非常に頑固ですが、多くの場合、ユーザーが提供するさまざまなポリシーやヒントの影響を受ける可能性があります。ほぼすべてのスケジューラのデフォルト設定は、使用可能なすべての CPU に負荷を分散しようとするものであり、その結果、ある CPU から別の CPU にプロセスが移行することがよくあります。幸いなことに、「最先端のデスクトップ OS」(別名 OS X) を除く最新の OS は、プロセッサ アフィニティと呼ばれるものをサポートしています。すべてのプロセスには、実行が許可されている一連のプロセッサがあります。これは、そのプロセスのいわゆる CPU アフィニティ セットです。ばらばらなアフィニティ セットをさまざまなプロセスに設定することで、互いに CPU 時間を盗むことなく、それらを同時に実行することができます。明示的な CPU アフィニティは、Linux、FreeBSD (ULE スケジューラを使用)、Windows NT (これには Windows XP 以降のすべてのデスクトップ バージョンも含まれます)、およびその他の OS (OS X を除く) でサポートされています。次に、すべての OS は、アフィニティを操作するための一連のカーネル呼び出しと、特別なプログラムを作成せずにそれを行うための手段を提供します。Linux では、これはsched_setaffinity(2)
システムコールとtaskset
コマンドラインインストルメント。cpuset
アフィニティは、インスタンスを作成することによっても制御できます。Windows では、SetProcessAffinityMask()
and/orSetThreadAffinityMask()
を使用し、特定のプロセスのコンテキスト メニューからタスク マネージャーでアフィニティを設定できます。また、新しいプロセスを開始するときに、希望するアフィニティ マスクをSTART
シェル コマンドのパラメータとして指定することもできます。
これがすべて OpenMP と関係しているのは、リストされている OS のほとんどの OpenMP ランタイムが、各 OpenMP スレッドに必要な CPU アフィニティを指定する何らかの形式または別の方法でサポートされているということです。最も単純な制御はOMP_PROC_BIND
環境変数です。これは単純なスイッチです。 に設定するとTRUE
、OpenMP ランタイムに各スレッドを「バインド」するように指示します。つまり、単一の CPU のみを含むアフィニティ セットを与えます。CPU へのスレッドの実際の配置は実装に依存し、各実装はそれを制御する独自の方法を提供します。たとえば、GNU OpenMP ランタイム ( libgomp
) はGOMP_CPU_AFFINITY
環境変数を読み取りますが、Intel OpenMP ランタイム (少し前からオープンソース) はKMP_AFFINITY
環境変数を読み取ります。
ここでの理論的根拠は、使用可能なすべての CPU のサブセットのみを使用するように、プログラムのアフィニティを制限できるということです。残りのプロセスは主に残りの CPU にスケジュールされますが、これはアフィニティを手動で設定した場合にのみ保証されます (ルート/管理者アクセスがある場合にのみ実行可能です。そうでない場合は、プロセスのアフィニティのみを変更できますやったね)。
アフィニティ セット内の CPU の数よりも多くのスレッドで実行することは、多くの場合 (常にではありません) 意味がないことに注意してください。たとえば、プログラムを 60 個の CPU で実行するように制限する場合、64 個のスレッドを使用すると、一部の CPU がオーバーサブスクライブされ、スレッド間でタイムシェアリングが発生します。これにより、一部のスレッドの実行が他のスレッドより遅くなります。ほとんどの OpenMP ランタイムのデフォルトのスケジューリングはschedule(static)
したがって、並列領域の合計実行時間は、最も遅いスレッドの実行時間によって決まります。あるスレッドが別のスレッドとタイムシェアする場合、両方のスレッドの実行速度がタイムシェアしないスレッドよりも遅くなり、並列領域全体が遅延します。これは並列パフォーマンスを低下させるだけでなく、より高速なスレッドが何もせずに待機するため、無駄なサイクルが発生します (並列領域の最後にある暗黙のバリアでビジー ループが発生する可能性があります)。解決策は、動的スケジューリングを使用することです。つまり、次のようになります。
#pragma omp parallel for schedule(dynamic,chunk_size)
for (int out = 1; out <= matrix.rows; out++)
{
...
}
ここchunk_size
で、各スレッドが取得する反復チャンクのサイズです。反復スペース全体が反復のチャンクに分割されchunk_size
、先着順でワーカー スレッドに与えられます。チャンク サイズは重要なパラメータです。値が低すぎる場合 (デフォルトは 1)、動的スケジューリングを管理する OpenMP ランタイムからのオーバーヘッドが非常に大きくなる可能性があります。値が高すぎると、各スレッドで使用できる作業が十分にない可能性があります。よりも大きなチャンクサイズを持つことは意味がありませんmaxtrix.rows / #threads
。
動的スケジューリングを使用すると、たとえば他のプロセスが実行されており、現在のプロセスとタイムシェアリングされている場合など、CPU リソースが均一でない場合でも、プログラムを使用可能な CPU リソースに適応させることができます。ただし、問題があります。64 コアのような大規模システムは、通常、ccNUMA (キャッシュ コヒーレント不均一メモリ アクセス) システムです。つまり、各 CPU には独自のメモリ ブロックがあり、そのメモリ ブロックにアクセスします。他の CPU はコストがかかります (たとえば、時間がかかり、帯域幅が少なくなります)。動的スケジューリングは、ある NUMA に存在するメモリのブロックが別の NUMA ノードで実行されているスレッドによって使用されないことを確認できないため、データの局所性を破壊する傾向があります。これは、データ セットが大きく、CPU キャッシュに収まらない場合に特に重要です。したがって、YMMV。