🐸

HWLOCを使ったプロセッサバインディング

2022/12/31に公開

0.概要

C++高速化の話です。

一部のスピード狂は、マルチコアプロセッサでミリ秒単位の処理時間の揺らぎを何とかしたいとき、スレッドが実行されるプロセッサの割当てを明示的に指定することで、コンテキストスイッチやキャッシュミスによるオーバーヘッドを低減させるテクニックを使います。

これは、プロセッサバインディング(あるいはスレッドアフィニティ)と呼ばれ、WindowsのSetThreadAffinityMask()、Linuxのsched_setaffinity()といったシステムコールを使って実現しますが、HWLOCでもできます。

HWLOCはコンパイラの違いによる実装差異を吸収してくれるだけでなく、CPUのキャッシュ階層や物理プロセッサと論理プロセッサ(ハイパースレッド有効時)のマッピング情報なども取得できるため、CPUの差異も加味した実装が楽になります。

この記事では、HWLOCを使ったプロセッサバインディングの短い実装例を紹介します。

1.システムコールを使った実装例

はじめに、HWLOCの実装例と比較するためシステムコールを使った実装例を示します。

この実装例では、バインド前のスレッドIDとプロセッサ番号のマッピング、およびバインド後のスレッドIDとプロセッサ番号のマッピングを表示します。

#if _MSC_VER
#include <Windows.h>
#else
#include <sys/types.h>
#include <sched.h>
#include <unistd.h>
#endif
#include <thread>
#include <iostream>

using namespace std;

// @brief 現在スレッド利用されているプロセッサ番号を表示する
void PrintUsingProccessor()
{
  constexpr int N = 30;
  for (int i = 0; i < N; ++i)
  {
    cout << "Thread id=" << this_thread::get_id() << ", "
         << "Processor="
#if _MSC_VER
         << GetCurrentProcessorNumber()
#else
         << sched_getcpu()
#endif
         << endl;
    this_thread::sleep_for(chrono::milliseconds(50));
  }
}

// @brief 指定されたプロセッサ番号に現在スレッドを固定する
// @param[in] processorNumber プロセッサ番号
void BindProcessor(int processorNumber)
{
#if _MSC_VER
  const int mask = 1 << processorNumber;
  SetThreadAffinityMask(GetCurrentThread(), static_cast<DWORD_PTR>(mask));
#else
  constexpr pid_t CUR_PROCESS = 0;
  cpu_set_t set;
  CPU_ZERO(&set);
  CPU_SET(processorNumber, &set);
  sched_setaffinity(CUR_PROCESS, sizeof(set), &set);
#endif
}

int main()
{
  constexpr int processorNumber = 0; // バインドするプロセッサ番号
  cout << "# Default" << endl;
  PrintUsingProccessor();
  BindProcessor(processorNumber);
  cout << "# Bind" << endl;
  PrintUsingProccessor();
  return 0;
}

Corei7 i7-7800X(論理コア12,物理コア6,ハイパースレッド有効)のでの実行結果は以下です。

# Default
Thread id=13176, Processor=2
Thread id=13176, Processor=6
Thread id=13176, Processor=8
Thread id=13176, Processor=8
Thread id=13176, Processor=2
Thread id=13176, Processor=0
Thread id=13176, Processor=0
Thread id=13176, Processor=2
...(略)
# Bind
Thread id=13176, Processor=0
Thread id=13176, Processor=0
Thread id=13176, Processor=0
Thread id=13176, Processor=0
Thread id=13176, Processor=0
Thread id=13176, Processor=0
...(略)

バインド前は表示毎にプロセッサ番号が変わるのに対して、バンド後はハードコーディングされた0番目のプロセッサを使っていることが確認できます。

2.HWLOCを使った実装例

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <thread>
#include <hwloc.h>

using namespace std;

// @brief 現在利用されている論理プロセッサ番号,物理プロセッサ番号を表示する
// @param[in] t トポロジーコンテキスト
void PrintUsingProccessor(hwloc_topology_t t)
{
  constexpr int N = 30;
  hwloc_bitmap_t b = hwloc_bitmap_alloc();
  hwloc_obj_t pu, core;
  for (int i = 0; i < N; ++i)
  {
    hwloc_get_last_cpu_location(t, b, HWLOC_CPUBIND_THREAD);
    pu = hwloc_get_pu_obj_by_os_index(t, hwloc_bitmap_first(b));
    core = pu->parent;
    cout << "Thread_id=" << this_thread::get_id() << ", "
         << "Processor=" << pu->logical_index
         << "(" << core->logical_index << ")"
         << endl;
    this_thread::sleep_for(chrono::milliseconds(50));
  }
  hwloc_bitmap_free(b);
}

// @brief 指定されたプロセッサ番号にスレッドを固定する
// @param[in] t               トポロジーコンテキスト
// @param[in] processorNumber プロセッサ番号
void BindProcessor(hwloc_topology_t t, int processorNumber)
{
  const int depthTop = hwloc_topology_get_depth(t);
  const hwloc_obj_t pu =
    hwloc_get_obj_by_depth(t, depthTop - 1, processorNumber);
  hwloc_set_cpubind(t, pu->cpuset, HWLOC_CPUBIND_THREAD);
}

int main()
{
  constexpr int processorNumber = 0;
  hwloc_topology_t t;
  hwloc_topology_init(&t);
  hwloc_topology_load(t);
  cout << "# Default" << endl;
  PrintUsingProccessor(t);
  BindProcessor(t, processorNumber);
  cout << "# Bind" << endl;
  PrintUsingProccessor(t);
  hwloc_topology_destroy(t);
}

Corei7 i7-7800X(論理コア12,物理コア6,ハイパースレッド有効)のでの実行結果は以下です。カッコ内に表示している番号は物理プロセッサ番号です。

# Default
Thread_id=5032, Processor=0(0)
Thread_id=5032, Processor=2(1)
Thread_id=5032, Processor=10(5)
Thread_id=5032, Processor=0(0)
Thread_id=5032, Processor=7(3)
Thread_id=5032, Processor=9(4)
Thread_id=5032, Processor=4(2)
...(略)
# Bind
Thread_id=5032, Processor=0(0)
Thread_id=5032, Processor=0(0)
Thread_id=5032, Processor=0(0)
Thread_id=5032, Processor=0(0)
Thread_id=5032, Processor=0(0)
Thread_id=5032, Processor=0(0)
...(略)

ポイントとなるバインド関数はhwloc_set_cpubind()です。システムコールの実装例と比べて、マクロ構文が無くすっきりしましたが、トポロジーというキラーキーワードが出てきました。

トポロジーは、HWLOCのチュートリアルのP.58にあるように、CPUソケット、キャッシュ構造、物理コア、論理コア等が階層化されたグラフ構造のことです。

hwloc_topology_get_depth()でこの階層数を取得し、hwloc_get_obj_by_depth()でバインドしたい論理プロセッサ番号に紐づくコンテキストを取得します。このコンテキストのメンバ変数であるcpusetを使ってバインドするという仕組みです。

3.まとめ

HWLOCを使ったプロセッサバインディングの実装例を示しました。

Windowではここから、ヘッダとDLLを持ってきてダイナミックリンクすれば使えます。

Linuxではsudo apt-get install -y libhwloc-devで使えます。

速度も移植性も可読性も大事にしたい欲張りさんにお勧めしたいです。

Discussion