HyperThreadingとAMD Bulldozerとカーネルスケジューリング

はじめに

本稿は、Intel HyperThreadingTechnologyとAMD Bulldozerアーキテクチャとカーネルスケジューリングの関係をテーマとしている。

疑問の発端は、AMD Bulldozerアーキテクチャが発表された際に、Windows7で並列処理のパフォーマンスが出ず、AMDがパッチを作ってその後修正されたという話を(今さら)思い出したことだ。

OS側の対応が必要ということは、他のOSはどうなっているのだろう、と思ったわけである。

AMD Bulldozerアーキテクチャ

AMD BulldozerはK10アーキテクチャの次世代アーキテクチャとして、AMDから発表されているアーキテクチャである。現世代のAMD FXシリーズや、AMD AシリーズAPUにはBulldozerの後継アーキテクチャが搭載されている。

特徴は、2つの整数演算コア、浮動小数点演算装置、命令デコーダ、L2キャッシュから構成される「モジュール」をひとつの単位とし、コアの増減はモジュール単位で行われることにある。見かたを変えれば、2つの演算コアが、ひとつの浮動小数点演算装置とL2キャッシュを共有しているとも言える。

たとえば、APUであるA10-7800は、Bulldozerの後継である Kaveriモジュールを2基、計4コアを搭載している。

Windows 7でのBulldozerアーキテクチャ対応

BulldozerはIntelのCore iシリーズの対抗馬として登場したが、実際のところそのパフォーマンスは振るわなかった。特にCore iシリーズよりも廉価に多くのコアを乗せられるのが特徴であるにもかかわらず、マルチプロセッサでの性能が出なかったのである。

AMDは当初、アーキテクチャが特殊なため、OSやアプリケーション側の対応が必要になると主張していた。実際、Windows 7に対してはBulldozer対応のパッチがWindows Update経由で配布された。

何が問題で、どう修正したのかを簡単に述べると以下のようになる。

コア0とコア1を持つモジュールAと、コア2とコア3を持つモジュールBからなる、4コアのシステムを考える。Bulldozerの場合、このシステムはフラットな4コアの CPUとしてOS側に見えてしまう。

OSがプロセスをコアに割り振る場合、空いているコアから割り振るため、最初にコア0に割り振ったら次にコア1に割り振る。

ところが、Bulldozerアーキテクチャでは、コア0とコア1とで、キャッシュ、命令デコーダ、浮動小数点演算装置を共有しているため、2コア分の働きをすることができない。

OSには、コア0(モジュールA)の次には、コア2(モジュールB)にプロセスを割り振ってもらいたいわけである。

これとよく似た問題が、Intel HyperThreadingTechnologty(HTT)でも発生する。HTTではスーパースケイラのうちの空いているパイプラインを有効活用するために、見せ掛けのコア数を増やしている。物理コア数に対して、論理コア数という概念を導入し、1コア上で2つの論理コアが使えるように見せる。もちろん、物理的には1コアであり、機能の多くを共有しているため、2コア分の働きをすることはできない。OS側がプロセスを割り振る場合には、この点を考慮する必要がある。

Windows 7でのBulldozer対応は、BulldozerにおいてもHTTと同様の処理をするようにした修正であると推測できる。OSのプロセススケジューリングの際に、4コアのBulldozerは、2物理コア(モジュール)、4論理コアのCPUとして扱われる。

ところが、これはこれで別の問題がある。

省電力実現のためにコアを休眠させる処理は、コア単位ではなくモジュール単位でしか行えない。これは、モジュール内で機能を共有しているので必然である。そうなると、それほど性能を実現せず、むしろ省電力を実現したい場合は、先ほどのストラテジーとは逆に、ふたつのプロセスを同じモジュール内のコアで走らせて、他のモジュールを休眠させたほうがよい。

このように、Bulldozerアーキテクチャでは、SMPではあるものの従来とは異なるスケジューリングが必要になる。

FreeBSDでのHyperThreadingのスケジューリング

既に述べたように、BulldozerとHTTは類似の問題を抱えている。

そこで、FreeBSDにおいてHTTの場合にどのようなスケジューリングを行っているかを、調べてみよう。

FreeBSDでは、CPUを「CPU group」ごとにまとめて管理している。cpu_group構造体はsys/smp.hで以下のように定義されている。

struct cpu_group {
      struct cpu_group *cg_parent;    /* Our parent group. */
      struct cpu_group *cg_child;     /* Optional children groups. */
      cpuset_t        cg_mask;        /* Mask of cpus in this group. */
      int32_t         cg_count;       /* Count of cpus in this group. */
      int16_t         cg_children;    /* Number of children groups. */
      int8_t          cg_level;       /* Shared cache level. */
      int8_t          cg_flags;       /* Traversal modifiers. */
};

このうち、cg_flagsがHTTなどのフラグであり、値は以下のように定義されている。

#define CG_FLAG_HTT     0x01            /* Schedule the alternate core last. */
#define CG_FLAG_SMT     0x02            /* New age htt, less crippled. */
#define CG_FLAG_THREAD  (CG_FLAG_HTT | CG_FLAG_SMT)     /* Any threading. */

CPUのトポロジーは、sysctlでkern.sched.topology_specの値を見ることで確認できる。以下は、Core i3-3217U (IvyBridge)での実行結果である。

kern.sched.topology_spec: <groups>
 <group level="1" cache-level="0">
  <cpu count="4" mask="f,0,0,0">0, 1, 2, 3</cpu>
  <children>
   <group level="2" cache-level="2">
    <cpu count="4" mask="f,0,0,0">0, 1, 2, 3</cpu>
    <children>
     <group level="3" cache-level="1">
      <cpu count="2" mask="3,0,0,0">0, 1</cpu>
      <flags><flag name="THREAD">THREAD group</flag><flag name="SMT">SMT group</flag></flags>
     </group>
     <group level="3" cache-level="1">
      <cpu count="2" mask="c,0,0,0">2, 3</cpu>
      <flags><flag name="THREAD">THREAD group</flag><flag name="SMT">SMT group</flag></flags>
     </group>
    </children>
   </group>
  </children>
 </group>
</groups>

上記のCore i3はdual coreのCPUで、コアごとにHTTで2つのスレッドが動作する(いわゆる2C4T)。sysctlの結果を見ると、ふたつのCPUグループがあり、それぞれに2つのSMT(Simultaneous MultiThreading)を動作できることが分かる。

次にスケジューラのソースを追跡しよう。参考にするソースコードは2015年4月時点のCURRENT(r281991)のものだ。現在のFreeBSDではULEスケジューラ(SCHED_ULE)を用いている。ULEスケジューラのソースコードは、kern/sched_ule.cになる。

スケジューリング処理は、以下の関数で行う。

void
sched_switch(struct thread *td, struct thread *newtd, int flags)

更にCPUの選択は、sched_switchから呼ばれる以下の関数にて行う。

static int
sched_pickcpu(struct thread *td, int flags)

では、sched_pickcpu()の処理を追ってみよう。

  1. 割込みスレッドは割込みが発生したCPUで動かす
  2. 以前スレッドが動いていたCPUでスレッドを実行可能で、かつAffinity (CPUアフィニティー)が期限切れになっていない場合は、そのCPUで動かす。
  3. 以前と同じキャッシュレベルに所属するCPUグループを探す。Affinityが期限切れのものと、SMTのものはスキップする。
  4. グループ内でもっとも負荷の低いCPUを探す
  5. 全体でもっとも負荷の低いCPUを探す

大別するとふたつの戦略が読みとれる。ひとつは、CPUアフィニティーを最大限に満たそうとする点、もうひとつは違うコアの選択をするときにSMTをスキップする点である。

CPUアフィニティーは、期限切になっていない場合は、同じスレッドに同じ同じCPUを割り当てようとする性質である。これは、以前と同じコアを使ったほうがキャッシュが残っている可能性が高く、効率がよいからである。

現在と異なるコア(論理または物理)を選択する時に、SMTをスキップするのは、SMTでないコアに空きがあるのであれば、そちらを選んだほうが性能が出る。この箇所が、「HTTのための特別な配慮」ということになる。

FreeBSDでのBulldozer対応状況

Bulldozerに関しては、HTTと同様の問題があるため、スケジューラは特別な配慮を行う必要がある。FreeBSDにおいて、Bulldozerアーキテクチャがどのように扱われているかを調べてみよう。

そのためには、cpu_group構造体がどのようにして構築されるのかを調べる必要がある。

amd86アーキテクチャでのCPUトポロジーの検出は、armd64/amd64/mp_machdep.c の中の、cpu_topo()で行っている。

cpu_topo()はtopo_probe()を呼び、更にその中でIntel CPUとAMD CPUとで異なった関数を呼ぶ。AMDの場合は、topo_probe_amd()である。

topo_probe_amd()の中では、最初に論理コア数を1に固定している。

後半では、Fam 10h(K10のことを指す)以降のコアについて、論理コアが存在したら、コア数(cpu_cores)をインクリメントしている。

cpu_topo()内の、cpu_group構造体にHTTやSMTのフラグを設定する箇所では、論理コアの個数(cpu_logical)を見ており、AMD CPUではこれは必ず1になるため、HTTやSMTのフラグは立たない。

これを見ると、FreeBSDでは、Bulldozerコアを2つのSMPとして扱っているようだ。

実際、Opteron3280 (Bulldozer 4モジュール、8コア)での、CPUトポロジーは以下のように認識されているようだ。

kern.sched.topology_spec: <groups>
 <group level="1" cache-level="0">
  <cpu count="8" mask="0xff">0, 1, 2, 3, 4, 5, 6, 7</cpu>
   <children>
    <group level="2" cache-level="2">
     <cpu count="8" mask="0xff">0, 1, 2, 3, 4, 5, 6, 7</cpu>
   </group>
  </children>
 </group>
</groups>

参考までにLinuxの場合であるが、ソースコードを確認はしていないものの、Wikipediaによれば、LinuxカーネルはBulldozerモジュールを、1物理コア 2論理コアのSMTとして認識するようだ。

結論とまとめ

Intel HTTとAMD Bulldozerアーキテクチャは、論理コアに対するプロセスの割り当てに処理において、特別な配慮を必要とする。

FreeBSDでは、HTTについてはスケジューリング時のCPU選択で配慮された処理がなされているが、AMD Bulldozerについては通常のマルチコアと同じ扱いになっている。

このため、AMD Bulldozerでは、正しい処理を行っているLinuxなどと比較して性能劣化が予想される。

実機が手に入ったら、カーネルへの修正を加えてみたいところだが、単純な予想をすると、ベンチマークのように全スレッドで処理を回す種類の処理だと、明確な差が出ないかもしれない。

記事執筆者: 木本雅彦
記事公開日:2015年05月28日