PAPI を使った

PAPI を使うと CPU のハードウェアカウンタを使って様々な値を測定できる。 例えば

  • 実行した命令の数
  • 実行した浮動小数点演算の数
  • パイプラインがストールした回数
  • キャッシュヒット/ミス の回数
  • 分岐予測の成功/失敗数

を測定できる。測定した値は FLOPS の測定なりパフォーマンスのチューニングなりに使える。

適切に設定すれば CPU 以外のハードウェアのカウンタも読めるらしいが、使ってないのでよくわからない。

インストール

昔はインストールするのにカーネルにパッチを当てる必要があったりして一苦労だったらしい (検索すると出てくる) が、 最新版の Linux カーネルであればインストールは ./configure; make; make install で済んだ。

利用可能なイベントの確認

papi_avail で利用可能なイベントの一覧を確認できる。コンシューマ向けの CPU だと性能測定用のハードウェアカウンタがしょぼいようなので、インストールしたら必ず確認したほうがいい。

自分の環境では以下のようになった。

$ papi_avail
Available PAPI preset and user defined events plus hardware information.
--------------------------------------------------------------------------------
PAPI version             : 5.6.1.0
Operating system         : Linux 4.4.0-128-generic
Vendor string and code   : GenuineIntel (1, 0x1)
Model string and code    : Intel(R) Core(TM) i7-7700 CPU @ 3.60GHz (158, 0x9e)
...
--------------------------------------------------------------------------------

================================================================================
  PAPI Preset Events
================================================================================
    Name        Code    Avail Deriv Description (Note)
PAPI_L1_DCM  0x80000000  Yes   No   Level 1 data cache misses
PAPI_L1_ICM  0x80000001  Yes   No   Level 1 instruction cache misses
PAPI_L2_DCM  0x80000002  Yes   Yes  Level 2 data cache misses
PAPI_L2_ICM  0x80000003  Yes   No   Level 2 instruction cache misses
PAPI_L3_DCM  0x80000004  No    No   Level 3 data cache misses
PAPI_L3_ICM  0x80000005  No    No   Level 3 instruction cache misses
...
PAPI_FML_INS 0x80000061  No    No   Floating point multiply instructions
PAPI_FAD_INS 0x80000062  No    No   Floating point add instructions
PAPI_FDV_INS 0x80000063  No    No   Floating point divide instructions
PAPI_FSQ_INS 0x80000064  No    No   Floating point square root instructions
PAPI_FNV_INS 0x80000065  No    No   Floating point inverse instructions
PAPI_FP_OPS  0x80000066  No    No   Floating point operations
PAPI_SP_OPS  0x80000067  Yes   Yes  Floating point operations; optimized to count scaled single precision vector operations
PAPI_DP_OPS  0x80000068  Yes   Yes  Floating point operations; optimized to count scaled double precision vector operations
PAPI_VEC_SP  0x80000069  Yes   Yes  Single precision vector/SIMD instructions
PAPI_VEC_DP  0x8000006a  Yes   Yes  Double precision vector/SIMD instructions
PAPI_REF_CYC 0x8000006b  Yes   No   Reference clock cycles
--------------------------------------------------------------------------------
Of 108 possible events, 59 are available, of which 18 are derived.

出力の最終行から、全体の約半分 (59 / 108) のイベントしか測定できないことが分かる。

なお PAPI_FP_OPS という浮動小数点演算の数を数えるための一番基本的なイベントが使えないことから、 PAPI のサンプルコードやテストが上手く実行できなかった。 浮動小数点演算の回数自体は PAPI_DP_OPS というイベントを使って測定できたのでよかったが、 一番最初に実行するサンプルコードが segmantation fault で落ちるので原因の調査にかなり時間を食った。

使う

以下のプログラムは浮動小数点演算の回数、CPU時間を計測し、そこから計算される GFLOPS と合わせて表示させた。 それから実行された命令の数も計測して表示させている。

#include <iostream>
#include <papi.h>
#include <vector>

void print_papi_error(int error_num) {
  std::cout << "PAPI error " << error_num << ": " << PAPI_strerror(error_num)
            << "\n";
}

int main() {
  int retval;
  auto a = 0.0;

  // 測定するイベント
  std::vector<int> events = {PAPI_TOT_INS, PAPI_DP_OPS};
  // 測定結果を収める場所
  std::vector<long_long> values(events.size());

  // カウンタをスタートさせる
  // events が多すぎる場合はここでエラーが出る
  retval = PAPI_start_counters(events.data(), events.size());
  if (retval != PAPI_OK) {
    print_papi_error(retval);
  }

  auto cpu_time_begin_usec = PAPI_get_virt_usec();
  // 浮動小数点演算を 10^8 回行う
  // 前後の命令が依存関係を持つので性能は出ない
  for (auto i = 0; i < 1.e8; i++) {
    a += 1;
  }
  auto cpu_time_end_usec = PAPI_get_virt_usec();

  // カウンタの値を読む
  // 値を読んでもカウンタは止まらない
  retval = PAPI_read_counters(values.data(), events.size());
  if (retval != PAPI_OK) {
    print_papi_error(retval);
  }

  // 測定結果の表示
  auto &number_of_executed_instructions = values[0];
  auto &number_of_floating_point_operation = values[1];
  auto elapsed_cpu_time_usec = cpu_time_end_usec - cpu_time_begin_usec;
  auto GFLOPS = 1.e6 * number_of_floating_point_operation / elapsed_cpu_time_usec;

  std::cout << "number_of_floating_point_operation = " << number_of_floating_point_operation << "\n";
  std::cout << "elapsed_cpu_time_usec = " << elapsed_cpu_time_usec << "\n";
  std::cout << "GFLOPS = " << GFLOPS * 1.e-9 << "\n";
  std::cout << "number_of_executed_instructions = " << number_of_executed_instructions << "\n";

  // カウンタをクリア
  PAPI_stop_counters(values.data(), events.size());
}
number_of_floating_point_operation = 100000000
elapsed_cpu_time_usec = 264001
GFLOPS = 0.378786
number_of_executed_instructions = 1200002473

プログラムの出力のうち、 number_of_floating_point_operation だけは何回測っても変わらないという素晴らしい性質を持つ。他の値は測るたびに変わってしまう。

参考

PAPI