Core Parking Internals - 解答編 & Interrupt Affinity Policy Tool の簡単な説明

Core Parking は万能ではないで出題した、Parked (保留) 状態のコアが CPU を利用している状態の解答編です。

実は Core Parking は負荷に応じて直接 CPU を Throttling State (省電力状態) に落とすわけではありません。
いくつかの段階を経て CPU を省電力状態へ落とすのです。過程は以下のようになります。

  1. システムの負荷に応じて最適な CPU数を選択する
  2. 余分な Core を Parked (保留) 扱いへ変更
  3. Unparked Core (通常状態の Core) を中心にスケジューリングを行う
  4. 結果的に Parked 状態の Core が Idle状態になる
  5. Idle状態が続いている Core を Throttling状態 (省電力状態) へ移行

さて、Windows 7 で Core Parking を利用する方法で紹介したように Windows 7 でも Core Parking は有効にできるのですが、
Multi-core だが Non-NUMA (複数のソケットを利用していない場合) にどのような問題があるでしょうか。

答えは Affinity Mask にあります。

SetProcessAffinityMask(), SetThreadAffinityMask() 関数により、明示的に特定の論理プロセッサに Thread を紐付けることができますが、
“Affinity” (親和性) という名前に似つかわしくなく強制的に特定の論理プロセッサへの紐付けとなります。

結果、Core Parking で用いられている 3. の動作は Affinity Mask を持つスレッドには適用されず
Core Parking は万能ではないの図の例で Affinity Mask は CPU0 のみになっているスレッドは
Parked (保留状態) の論理プロセッサで実行されます。このため両方の Core が使用率 100% となっていたのです。

 

以前は L3 Cache を CPU パッケージの外側に置くことが多かったのですが、Memory Access の Latency が
性能に対し支配的になった現在は CPU の Cache は CPU の Core から非常に近い場所に置かれるようになりました。

そのため、現代の(小規模な) NUMA Architecture は以下の図のように構成されています。

image

Cache Coherent とは、Processor 0, 1 内の各キャッシュの内容が同時に変更されても”一貫性”を保てることを意味しています。
(ただし、Atomic 命令を用いない場合には同一の対象への読み書きが一貫性を喪失する可能性があります。)
Processor 1 が Processor 0 に接続された Memory を参照する際にはこの Cache Coherent Interconnect を利用することになります。
その動作は Processor 1 に直接接続された Memory を参照するよりも低速なものとなります。
そのため、このような構成を NUMA (Non-Uniform Memory Access; 非均一メモリアクセス) と呼びます。

※Cache Coherent に関しては 【コラム】コンピュータアーキテクチャの話 (13) キャッシュの構造や働き(上級編) - キャッシュコヒーレンシ (外部) が詳しいです。

Windows Server 2003 以前でも Interrupt Affinity Tool を用いて各NIC (Ethernet Controller) の割り込み処理を行う
論理プロセッサを明示的に指定することがあったと思いますが、Windows Server 2008, Windows 7 からは新しい Interrupt Affinity Policy ツールが提供されています。
新しい Interrupt Affinity Policy Tool では、論理プロセッサへの指定ではなく、デバイスとの距離を考慮した設定が可能になっています。

このように、NUMA を意識したプログラミングでは Affinity Mask を論理プロセッサに対して明示的に設定するのではなく、
その論理プロセッサがどのような配置をなされているかを意識する必要があります。

Core Parking を有効に利用するためには、SetProcessAffinityMaskUpdateMode() の利用を、
I/O 処理が中心で、特定のプロセッサにスケジュールさせたい場合には GetNumaNumberFromHandle() から I/O の対象に近いノードを取得し
SetThreadIdealProcessorEx() を利用する、といった工夫が必要になります。

もちろん何も指定しなくとも OS側でスケジューリングの調整は行われますが、I/O 処理に性能が強く影響される場合などはこういった API を活用していく必要があるでしょう。