64 コアを超える Windows 環境でマルチスレッド・プログラミングをしてみる

同カテゴリーの次の記事

ムーアの法則を超えるパフォーマンス向上率への対応: インテル® Cluster Studio XE

この記事に関するフォーラムに参加する

64ビット版の Windows 7 や Windows Server 2008 R2 では、システムに 64コアを超える論理プロセッサーが搭載されていると、プロセッサーはプロセッサー・グループに分割されます。これまでこの規則に該当するシステムはほとんどありませんでしたが、インテル® Xeon® プロセッサーの E7 ファミリー(いわゆる Westmere-EX)の一部は、10個の物理コアを搭載し、4ソケットのシステムでは40物理コア、そしてハイパースレッディングを有効にすると、80論理コアを搭載するシステムになります。

この記事では、NUMA 環境におけるプロセッサー・グループの導入が、マルチスレッド・プログラミングにどのような影響を及ぼすか検証してみます。

ここでは、Windows Server 2008 R2 Enterprise SP1 (build 7601) を使用しています。

論理コアが 80個搭載されたシステムで CPU 利用率の履歴を表示

プロセッサー・グループとは?

マイクロソフト社はかねてより、64論理コアを超える 64ビットWindows 環境ではプロセッサー・グループを採用することを表明していました。Windows において 64個を超えるプロセッサーを搭載するシステムのサポートに関しては、次のドキュメントを参照ください。

http://msdn.microsoft.com/ja-jp/windows/hardware/gg463349
http://technet.microsoft.com/ja-jp/windowsserver/ee661585

HPC で利用される OS 環境では、1つのアプリケーションがシステム上のすべてのプロセッサー・コアを占有することができます。しかし、エンタープライズ系のシステムでは他のアプリケーションやサービスに影響を与えるため、すべてのアプリケーションが均等にプロセッサー・リソースを利用できるよう、Windows ではプロセッサー・グループが導入されました。

80コアを搭載する Windows システムでは、ブート時に2つのプロセッサー・グループが生成されます。Windows Server 2008 R2 Enterprise SP1 では、2つのプロセッサー・グループはそれぞれ40コアで構成されます。

SP1以前の 64ビット Windows Server 2008 R2 Enterprise システムでは、プロセッサー・グループの管理に問題があり、80コアが 60コアと 20コアの 2つのグループに分割されてしまいます。そのため、アプリケーションが起動されたときにどちらのグループに属するかによって、利用できるコア数が異なってしまいます。64コアを超えるシステムでは、SP1が導入されていることを確認しましょう。これに関する情報は KB2510206 として公開されています。

http://support.microsoft.com/kb/2510206/ja

自分が使えるプロセッサーの個数は?

[タスク マネージャ] を起動し、「プロセス」の一覧でプロセスを選択し、右クリックして「関係の設定」を開くと、そのプロセスが使用可能なプロセッサー・コアが表示されます。プロセッサー・グループが導入されているシステムでは、「プロセッサー・グループ」に属するコアが表示され、起動中のプロセスがどちらのグループに属しているか確認できます。

各プロセッサーはノードとして区別されこの例では、グループ1にノード2と3、グループ0にノード0と1が属しています。ハイパースレッドが有効なシステムでは、10コアのインテル® Xeon® E7 ファミリーでは、ノードに20コアが確認できます。

グループ1のCPU グループ0のCPU

通常のマルチスレッド・アプリケーションは、プロセッサー・グループが導入されたシステムでも動作上問題はありませんが、明示的にスレッド数を極端に多く設定している場合は、オーバーサブスクライブ(要求過多)状態になる可能性があります。明示的に 64コアを生成するような場合、明らかにこのような状態になります。

Windows API を利用してコア数を取得

生成するスレッドの個数を動的に変更するには、アプリケーションが起動されたシステムのプロセッサー・コア数を取得して、その数を最大スレッドとして利用することがあります。プロセッサー・コアの数を取得するにはいくつかの方法があります。次の3つは伝統的に Windows 環境で利用されています。

①   GetSystemInfo() API で取得したSYSTEM_INFO構造体の dwNumberOfProcessors を見る。

②   GetNativeSystemInfo() API で取得したSYSTEM_INFO構造体のdwNumberOfProcessorsを見る。

③   環境変数NUMBER_OF_PROCESSORS を取得する。

次に GetSystemInfo() と GetNativeSystemInfo() の利用例を示します。GetNativeSystemInfo() は、64 ビット Windows 向けに追加された API です。
#include <windows.h>
#include <winbase.h>

int main(){
SYSTEM_INFO sys1, sys2;

     GetNativeSystemInfo(&sys1);
     GetSystemInfo(&sys2);
     printf("GetNativeSystemInfo %d\n", sys1.dwNumberOfProcessors);
     printf("GetSystemInfo %d\n", sys2.dwNumberOfProcessors);
}

上記の2つの API では、アプリケーションが属するプロセッサー・グループのプロセッサー・コア数を正しく取得できるので、dwNumberOfProcessors から取得した値をスレッド生成の最大数として利用しても問題ありません。前述のサンプルを 80コア搭載する Windows Server 2008 R2 Enterprise SP1 64ビット環境で実行すると、次のような結果を取得できます。

CreateThread() や__beginthreadex() を利用してスレッドを生成する場合、自分の属するグループのコア数を上回ってスレッドを生成すると、グループ内のプロセッサー・コアをスケジュールによって奪い合うことになります。このままでは、他のプロセッサー・グループのコアを利用することはできません。どうしてもシステムのすべてのプロセッサー・コアを利用したい場合、自分の属するグループのコア数を超えるスレッドは、CreateRemoteThread() 関数を利用して他のプロセッサー・グループに属する別プロセスのスレッドとしてスレッドを生成できます。

OpenMP を利用する場合

OpenMP のランタイム環境では、ランタイムシステムがシステム上の利用可能なプロセッサー・コア数を取得し、その値が生成するスレッドの最大数として自動的に設定されます。そのためプログラム中で明示的にスレッド数を制御していない場合、バイナリーをそのまま実行しても問題ありません。

Visual Studio 2005 Pro 以降でサポートされる OpenMP を利用してスレッド・プログラミングを行っている場合、起動されたアプリケーションのプロセスが利用できるプロセッサー・コア数には自動的にプロセッサー・グループに属するコア数が設定されます。

インテル® コンパイラーを利用する場合注意が必要です。インテル® コンパイラーの OpenMP ランタイムは、プロセッサー・グループに属するコア数を正しく取得できません(バージョン12.1.0.233 、2011年8月リリース時点)。この問題は次期バージョンで修正される予定です。

次のサンプルコードを Visual C++ とインテル® コンパイラーでコンパイルして実行してみます。

#include <omp.h>
#include <windows.h>
#define MAX 1024
float a[MAX], b[MAX];

int main(){
int i, ct; char buf[32];

for(i=0; i<32; i++) buf[i] = (char) NULL;
ct = GetEnvironmentVariable("OMP_NUM_THREADS", buf, 32);
printf("OMP_NUM_THREADS = %s\n", buf);

printf("omp_get_num_procs %d\n", omp_get_num_procs());
#pragma omp parallel
  {
     if(omp_get_thread_num() == 0)
          printf("omp_get_num_threads %d\n", omp_get_num_threads());
#pragma omp for
     for(i=0; i<MAX; i++)
          a[i] += b[i] * i;
  }
}

Visual C++ の OpenMP ランタイムは、omp_get_num_procs() と omp_get_num_threads() API でそれぞれ40を取得できますが、インテル® コンパイラーの OpenMP ランタイムでは80を取得してしまいます。また実行時にランタイムエラーが表示されることがあります。この場合、OMP_NUM_THREADS 環境変数に40を設定することで問題を回避できます。

新しく追加された API と機能

NUMA システムで動作する Windows 7 や Windows Server 2008 R2 では、CMD.exe の拡張機能として start コマンドで /NODE と /AFFINITY オプションを利用することができます。この機能を利用すると、アプリケーション起動時に静的に実行するノードとノード内のどの論理コアを利用するかアフィニティーを設定できます。

例えば、start /NODE 1 omp_icl.exe とすると、アプリケーション(omp_icl.exe)はノード1に割り当てられます。ただし、/AFFINITY はプロセッサー・グループが1つしかないときは思い通りに論理プロセッサーを割り当てることができますが、1つ以上のグループではグループ番号とマスクでタプルされます。

もともと Windows で AFFINITY は 64ビットの整数であるため、64個の論理プロセッサーしか管理できません。そのため、Windows7 以降の API では KAFFINITY 型が拡張されています。

64個を超える論理プロセッサー、グループ、ノード、アフィニティーを管理するため以下の API が追加されています。

CreateRemoteThreadEx() アプリケーションがデフォルトのスレッド・グループのアフィニティーを変更することを許します。
GetActiveProcessorCount() グループもしくはシステム上の利用可能な論理プロセッサー数を返す。
GetActiveProcessorGroupCount() システム上のグループ数を返す。
GetCurrentProcessorNumberEx() 呼び出したスレッドが実行される論理プロセッサーを示すPROCESOR_NUMBER構造体を返す。
GetLogicalProcessorInformation() システム上のすべての論理プロセッサーに関する情報を返す。
GetMaximumProcessorCount() グループもしくはシステムの最大論理プロセッサー数を返す。
GetMaximumProcessorGroupCount() システム上の最大グループ数を返す。
GetNumaAvailableMemoryNodeEx() 指定されたノードで利用可能な最大メモリー容量を返す。
GetNumaNodeProcessorMaskEx() 指定されたノードのすべての論理プロセッサーのグループ・アフィニティー・マスクを返す。
GetNumaProcessorNodeEx() 指定された論理プロセッサーが属するノードの番号を返す。
GetNumaProximityNodeEx() 指定された近接識別子のノード番号を返す。
GetProcessorGroupAffinity() 現在のグループのプロセス・アフィニティーを返す。
GetThreadGroupAffinity() 現在のグループのスレッド・アフィニティーを返す。
QueryIdleProcessorCycleTimeEx() 指定されたグループの論理プロセッサーのアイドルスレッドのサイクル時間を返す。
SetThreadGroupAffinity() グループ中の論理プロセッサーのスレッド・アフィニティーを設定する。

また、この拡張に伴い影響のある既存の API が変更されています。

既存のアプリケーションで下記に該当する場合、アプリケーションを変更することを考慮しなければいけません。

  1. アプリケーションがシステム上のプロセッサー情報を取得もしくは変更する場合、64倫理コア以上をサポートするため修正が必要。プロセッサーの利用率を表示する [タスク マネージャ] のようなアプリケーションが該当します。
  2. パフォーマンスを重要とし64論理コアを超えるプロセッサーでもスケールことが要求される、データベースのようなアプリケーション。
  3. プロセッサーごとのデータ構造を持つような DLL を利用するアプリケーションで、DLL が64論理コアをサポートするように修正されていない場合。アプリケーション中のすべてのスレッドは DLL でエキスポートされる関数を呼び出す場合、同じグループに割り当てられなければいけません。

また、プロセスやスレッドを生成する際にアフィニティーを制御している場合、影響がないか調査する必要があります。詳細は前述の URL で紹介した以下のドキュメントを参照してください。

Supporting Systems That Have More Than 64 Processors Guidelines for Developers November 5, 2008

本稿は Windows 環境において64個を超える論理プロセッサーを持つNUMAシステムでのプログラミング上の注意点を紹介しました。このトピックに関する情報はWeb上でもあまり公開されていないので、今後いろいろ検証して iSUS で紹介していこうと思っています。

関連記事

  • インテル® Xeon Phi™ コプロセッサーへの Windows* 初期対応インテル® Xeon Phi™ コプロセッサーへの Windows* 初期対応 この記事は、インテル® デベロッパー・ゾーンに掲載されている「Windows* early enabling for Intel® Xeon Phi™ Coprocessors」の日本語参考訳です。 *** 2013 年 9 月の最新情報 *** ドライバーとサポートするインテル(R) […]
  • Direct3D 12 概要 パート 8: CPU の並列性Direct3D 12 概要 パート 8: CPU の並列性 この記事は、インテル® デベロッパー・ゾーンに公開されている「Direct3D 12 Overview Part 8: CPU Parallelism」の日本語参考訳です。 パート 7 では、ダイナミック・ヒープについて触れ、それがどのように CPU の並列性に役立つか説明しました。それでは、これまで紹介した D3D 12 […]
  • Direct3D 12 概要 パート 6: コマンドリストDirect3D 12 概要 パート 6: コマンドリスト この記事は、インテル® デベロッパー・ゾーンに公開されている「Direct3D 12 Overview Part 6: Command Lists」の日本語参考訳です。 これまで、バンドル、PSO、記述子ヒープ&テーブルを通して、D3D 12 がどのように CPU […]
  • Windows* の OpenCL* 環境におけるインテル® INDE のクイック・インストール・ガイドWindows* の OpenCL* 環境におけるインテル® INDE のクイック・インストール・ガイド この記事は、インテル® デベロッパー・ゾーンに公開されている「Quick Installation Guide for OpenCL™ Development on Windows* with Intel® INDE」の日本語参考訳です。 この記事の PDF 版はこちらからご利用になれます。 インテル® INDE […]
  • 小惑星と DirectX* 12: パフォーマンスと省電力小惑星と DirectX* 12: パフォーマンスと省電力 この記事は、インテル® デベロッパー・ゾーンに公開されている「Asteroids and DirectX* 12: Performance and Power Savings」の日本語参考訳です。 サンプルコードのダウンロード (Web サイト) インテルが開発した小惑星 (asteroids) […]