プログラムの高速化に取り組んでいると、キャッシュヒット率や分岐予測ミス率、サイクルあたりの命令実行数など、詳しい性能情報を知りたくなります。 そういった情報は Linux であれば perf で取得できます。例えば、プログラムのサイクル数、実行命令数、L1 データキャッシュミス数を調べるには、次のように実行します。

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

しかし、perf stat はプログラム全体の情報を集計するため、特定のコード区間のプロファイリングには向いていません。 例えば、次のようなプログラムで compute_kernel() 単体の性能を計測しようとしても、perf stat の集計結果には initialize() の分が含まれてしまいます。

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

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

そこで、特定のコード区間のパフォーマンスイベントを計測するライブラリ perf-counter を作成しました。 本稿では基本的な使い方を紹介し、より実践的な使い方や内部実装については別の記事で扱う予定です。

perf-counter の使い方

perf-counter で性能を計測する手順は、次の 4 ステップから成ります。

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

例えば compute_kernel() の実行にかかるサイクル数は次のようにして計測できます。

 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_type と event_config:perf_event_open(2) に掲載されている type と config の組み合わせを使用して、計測するイベントを指定します。
  • group_fd:単体のイベントを計測する場合は -1 を指定します。複数のイベントを同時に計測する場合は、最初のカウンタを -1 で開き、他のカウンタには最初のカウンタの fd を指定します。

event_type と event_config の組み合わせには例えば次のようなものがあります。

event_type event_config 計測内容
PERF_TYPE_HARDWARE PERF_COUNT_HW_CPU_CYCLES サイクル数
PERF_TYPE_HARDWARE PERF_COUNT_HW_INSTRUCTIONS 実行命令数
PERF_TYPE_HARDWARE PERF_COUNT_HW_BRANCH_INSTRUCTIONS 分岐命令数
PERF_TYPE_HARDWARE PERF_COUNT_HW_BRANCH_MISSES 分岐予測ミス数
PERF_TYPE_SOFTWARE PERF_COUNT_SW_PAGE_FAULTS ページフォルト数

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

計測するイベントによっては複数の定数を組み合わせて 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);

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

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 7 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-counter で命令列の実行にかかるサイクル数を計測し、命令数との比を計算します。 完全なコードは 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
...
スループットの計測

スループットの計測

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

ビルドと実行

サンプルプログラムは -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.000 サイクル/命令、スループット 2.000 命令/サイクルという結果が得られました。 これらは uops.info の値と一致しています。

参考