はじめに

プログラムを速くしたいと思ったら、最初にやるべきことは現状の性能を計測することです。 そして改善策を考えて実装し、再び計測して効果を確かめる、ということを繰り返します。 高速化というのは、性能計測で始まり性能計測で終わるのです。

計測の方法は様々ありますが、最もシンプルなのは、対象の処理の前後にタイマー(C++ であれば std::chrono など)を埋め込み、処理にかかる時間を測ることです。 実際のところ、多くの場合はこの方法で事足ります。しかし、プロセッサの性能を限界まで引き出したい場合には、次のような要求が生じるため、タイマー埋め込みでは不十分です。

  • 命令数やサイクル数を計測して、理論性能と比較したい
  • キャッシュヒット率や分岐予測ミスの発生率など、性能改善の手がかりとなる情報を集めたい

Linux であれば、こうした情報は perf コマンドで取得できます。例えば、プログラムのサイクル数、実行命令数、L1 データキャッシュミス数を知りたければ、次のように実行すればよいです。

1
perf stat -e cycles,instructions,L1-dcache-load-misses ./my_program

しかし、perf statプログラム全体の情報を集計するため、特定のコード区間のプロファイリングには向いていません。 次のようなプログラムで compute_kernel() だけを計測したくても、initialize() の分まで集計されてしまい、 compute_kernel() 単体の情報が得られません。

1
2
3
4
5
...
initialize(data, n);     // 前処理

compute_kernel(data, n); // メイン処理
...

そこで作成したのが、プログラム中の特定のコード区間だけを対象にハードウェアイベントを計測するライブラリ perf-counter です。 x86-64 の rdpmc 命令によりシステムコールを介さず低オーバーヘッドでイベントの発生回数を計測します。

本稿では、perf-counter の基本的な使い方を紹介します。より実践的な使い方や内部実装については、別の記事で扱う予定です。

基本的な使い方

perf-counter で特定のコード区間の性能を計測する流れは、次の4ステップです。

  1. 計測したいイベントを指定してカウンタを開く
  2. カウンタを有効化する
  3. 対象のコード区間の前後でカウンタの値を読む
  4. カウンタを無効化・解放する

カウンタが有効化されると、イベントが発生するたびに値がインクリメントされます。 そのため、あるコード区間の前後でカウンタの値を読んで差し引くと、その間に発生したイベントの回数が求まります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 1. サイクル数を計測するカウンタを開く
struct perf_counter pc = perf_counter_open_by_id(
    PERF_TYPE_HARDWARE,       // event_type
    PERF_COUNT_HW_CPU_CYCLES, // event_config
    -1                        // group_fd
);

// 正しく開けたかチェックする
if (!perf_counter_is_valid(&pc)) {
    fprintf(stderr, "Failed to open a cycle counter.\n");
    return 1;
}

// 2. カウンタを有効化する
perf_counter_enable(&pc); // イベントの計測を開始する

// 3. compute_kernel() 呼び出し前後のカウンタの値を読む
uint64_t start = perf_counter_read(&pc);
compute_kernel();
uint64_t end = perf_counter_read(&pc);

// compute_kernel() にかかるサイクル数を出力する
printf("cycles: %lu\n", end - start);

// 4. カウンタを無効化・解放する
perf_counter_disable(&pc); // イベントの計測を終了する
perf_counter_close(&pc);

perf_counter_open_by_id(event_type, event_config, group_fd) の各引数の意味は次の通りです。

  • event_typeevent_configperf_event_open(2) のマニュアルページに掲載されている typeconfig の組み合わせを使用して、計測するイベントを指定します。
  • group_fd:単体のイベントを計測する場合は -1 を指定します。複数のイベントを同時に計測する場合は、最初のカウンタを -1 で開き、他のカウンタには最初のカウンタの fd を指定します。

イベント名を指定してカウンタを開く

サイクル数のような基本的なイベントは単一の定数で指定できますが、イベントによっては複数の定数を組み合わせて event_config を構築する必要があります。例えば L1 データキャッシュミスを計測したい場合、次のようなコードを書かなければならず、なかなか面倒です。

1
2
3
4
5
6
uint64_t config =
    PERF_COUNT_HW_CACHE_L1D
    | (PERF_COUNT_HW_CACHE_OP_READ << 8)
    | (PERF_COUNT_HW_CACHE_RESULT_MISS << 16);

struct perf_counter pc = perf_counter_open_by_id(PERF_TYPE_HW_CACHE, config, -1);

そこで便利なのが perf_counter_open_by_name() です。この関数は libpfm4 がインストールされた環境で perf-counter をビルドすると使えるようになり、イベント名でカウンタを開けます。 L1 データキャッシュミスであれば、次のように指定できます。

1
struct perf_counter pc = perf_counter_open_by_name("perf::L1-DCACHE-LOAD-MISSES", -1);

指定可能なイベント名の一覧は、付属の list_all_events を実行すると取得できます。 ビルド方法は後述の「ビルドと実行」を参照してください。

1
./build/examples/list_all_events events.txt  # events.txt にイベントの一覧を保存

出力の各行はイベント名とその説明から成ります。

...
perf::L1-DCACHE-LOADS # L1 cache load accesses

perf::L1-DCACHE-LOAD-MISSES # L1 cache load misses
...

perf_counter_open_by_name() のもう一つの利点は、プロセッサ固有のイベントも簡単に計測できることです。 例えば、AMD Ryzen 4700U (Zen 2) では、次のようにして SSE/AVX の浮動小数点乗算の回数を計測できます。

1
2
struct perf_counter pc = perf_counter_open_by_name(
    "amd64_fam17h_zen2::RETIRED_SSE_AVX_FLOPS:MULT_FLOPS", -1);

実例:FMA 命令の性能を計測する

perf-counter を用いた性能計測の実例として、AVX2 の FMA(Fused Multiply-Add)命令のレイテンシとスループットを計測してみます。 完全なコードは perf_avx2_fma.cpp を参照してください。

計測方法

レイテンシの計測は、依存関係のある FMA 命令をたくさん並べることで行います。前の命令が終わるまで次の命令が実行できないため、1命令ずつ逐次的に実行されます。経過サイクル数を命令数で割れば、1つの命令の実行に何サイクルかかるか(CPI)というレイテンシが計測できます。

1
2
3
4
vfmadd231ps ymm0, ymm1, ymm1  ; ymm0 = ymm0 + ymm1 * ymm1
vfmadd231ps ymm0, ymm1, ymm1  ; ymm0 = ymm0 + ymm1 * ymm1
vfmadd231ps ymm0, ymm1, ymm1  ; ymm0 = ymm0 + ymm1 * ymm1
...
レイテンシの計測

レイテンシの計測

スループットの計測は、依存関係のない FMA 命令をたくさん並べることで行います。各命令は互いに独立しているため、並列に実行できます。命令数を経過サイクル数で割れば、サイクルあたり同時に何命令実行できるか(IPC)というスループットが計測できます。

1
2
3
4
vfmadd231ps ymm1, ymm0, ymm0  ; ymm1 = ymm1 + ymm0 * ymm0
vfmadd231ps ymm2, ymm0, ymm0  ; ymm2 = ymm2 + ymm0 * ymm0
vfmadd231ps ymm3, ymm0, ymm0  ; ymm3 = ymm3 + ymm0 * ymm0
...
スループットの計測

スループットの計測

OS のスケジューリングや割り込みによるノイズを排除するため、いずれの計測も3回のウォームアップ実行の後に10回実行し、最小値を採用することにします。

ビルドと実行

サンプルプログラムは -DPERF_COUNTER_BUILD_EXAMPLES=ON を付けることでビルドされます。

1
2
3
cmake -B build -DCMAKE_BUILD_TYPE=Release -DPERF_COUNTER_BUILD_EXAMPLES=ON
cmake --build build
./build/examples/perf_avx2_fma

実行結果

実行環境:AMD Ryzen 7 4700U (Zen 2)、GCC 15.2.1

AVX2 vfmadd231ps - Latency
  Instructions : 1200000
  Best Cycles  : 6000062
  CPI          : 5.000
  IPC          : 0.200

AVX2 vfmadd231ps - Throughput
  Instructions : 1200000
  Best Cycles  : 600066
  CPI          : 0.500
  IPC          : 2.000

レイテンシは5サイクル/命令、スループットは2命令/サイクルという結果が得られました。 これらはuops.infoの値とも一致しており、高い精度で計測できていることがわかります。

まとめ

  • perf-counter は、指定したコード区間内のハードウェアイベントの発生回数を低オーバーヘッドで計測できるライブラリです。
  • また、プロセッサ固有のイベントも手軽に計測できます。
  • より実践的な使い方や内部実装については、今後、別の記事で解説予定です。

参考