LoginSignup
3
2

perf-statによるPMCトレース

Last updated at Posted at 2021-12-20

Perf Linux ProfilerでCPUパフォーマンスカウンタ(PMCs)をトレースする方法を調べました。
基本的な内容にはなりますが、本稿はperf-statによる測定方法と、それを利用したキャッシュレイテンシ計測についてのメモです。

Perf Linux Profilerについて

Perf Linux Profiler(以下perf)は、Linux kernelの一部でシステムの情報を取得するツールです。PCL(Performance Counters for Linux)、LPE(Linux perf events)、perf_eventsなどとも呼ばれます。図のように、アプリケーションからドライバ、CPU、メモリなど各種情報を取得・計測することができます。
perfには、liststatreportなどのサブコマンドがあり、それぞれのオプションも多数あります。機能の詳細はmanual pageなどにまとまっています。
今回はperf stat(以下、perf-stat)でコード全体の統計情報を取得します。

Event Sources
図1 Parf Event Sources, Brendan's siteから引用

perf-statによる計測の基本

  • インストール
    perfを使うためにはlinux-tools-commonパッケージをインストールする必要があります。環境によっては、これに加えてkernelに応じた関連パッケージ(linux-tools-kernelversion)のインストールを求められることもあります。
    また、kernelイベント情報を取得するためには/proc/sys/kernel/perf_event_paranoid1以下である必要があります。これは、ユーザ特権に応じてパフォーマンス監視を制限するキーで、デフォルトは2以上の場合が多いので注意が必要です。root権限で変更します。(dockerコンテナの場合はさらに起動時に--privilegedオプションが必要です)
    perf_event_paranoidの特権は取得するイベントによっては必ずしも必要はありません。
$ sudo su -c 'echo 1 > /proc/sys/kernel/perf_event_paranoid'
  • 取得できるイベントの確認
    perf-statで取得できるイベントの一覧はperf listで確認できます。
    この一覧にはアーキテクチャ固有のパフォーマンスイベントは含まれていませんが、イベントコード(Event Num. + Umask Value)が分かれば、それを指定することで取得できます。これらのコードはアーキテクチャマニュアルに載っている場合が多いです。
$ perf list | grep L1-dcache
  L1-dcache-loads                                    [Hardware cache event]
  L1-dcache-load-misses                              [Hardware cache event]
  L1-dcache-stores                                   [Hardware cache event]
  L1-dcache-store-misses                             [Hardware cache event]
  L1-dcache-prefetches                               [Hardware cache event]
  L1-dcache-prefetch-misses                          [Hardware cache event]
  • 計測
    perf stat-eオプションでイベントを指定して取得します。
    [events]の部分はカンマ区切りで複数指定可能です。
$ perf stat -e [events] [other options] ./a.out

実験:キャッシュアクセスレイテンシの測定

perf-statの使用方法が分かったので、実際に測定をしてみます。
今回は以下のコードで、L1Dキャッシュミスの値(L1-dcache-load-misses)と、サイクル数(cycles)の測定をします。
また、結果からL2キャッシュアクセスのレイテンシを計算してみます。

// code.c
// on L1, on L2のデータへのアクセスを行うプログラム
// L1キャッシュサイズのデータ"d1"とL1+L2キャッシュサイズのデータ"d2"を用意し
// それぞれのデータにアクセスするだけのコードです
// どちらのデータにアクセスするかはオプション引数"L1"、"L2"で指定します

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <string.h>

#define L1D_CACHE_SIZE 30*1024   // L1Dキャッシュサイズ(32K per core)以下
#define L2_CACHE_SIZE 200*1024   // L2キャッシュサイズ(256K per core)以下
#define ATTEMPTS 10e7            // アクセス回数

// ex: 配列データを標準出力する関数
void ex(const char* d, const int len) {
  for(int i = 0; i < len; ++i) {
    unsigned char c = d[i];
    printf("%02x", c);
  }
  printf("\n");
}

// main
// オプション引数で以下のどちらかを指定
// "L1": on L1のデータにアクセス
// "L2": on L2のデータにアクセス
int main(int argc, char* argv[]) {
  size_t attempts = ATTEMPTS;

  // オプション引数のチェック
  char* measure = argv[1];
  if(argc < 2) { puts("Not enough args."); abort(); }
  if(strcmp(measure, "L1") != 0 && strcmp(measure, "L2") != 0) { 
    puts("Invalid args. Only L1 or L2."); abort();
  }  
  
  // ランダム値を配列に格納
  char d1[L1D_CACHE_SIZE], d2[L1D_CACHE_SIZE+L2_CACHE_SIZE];
  int fd = open("/dev/urandom", O_RDONLY);
  if(fd < 0) { perror("open"); abort(); }
  int ret = read(fd, &d1, L1D_CACHE_SIZE);
  if(ret < 0) { perror("read"); abort(); }
  ret = read(fd, &d2, L1D_CACHE_SIZE+L2_CACHE_SIZE);
  if(ret < 0) { perror("read"); abort(); }
  
  // 値の確認(コンパイラ最適化で省かれないようにアクセスしておく)
  ex(d1, 10);
  ex(d2, 10);

  // 配列へのアクセス(on L1の場合)
  // アクセス位置はランダム
  if(strcmp(measure, "L1") == 0) {
    while(attempts--) {
      int64_t offset = rand()%(L1D_CACHE_SIZE);
      __asm__ volatile(
          "mfence\n\t"
          "mov %0, %%al\n\t"
          "mfence\n\t"
          :
          :"m"(d1[offset])
          );
    } 
  }

  // 配列へのアクセス(on L2の場合)
  // アクセス位置はランダム
  if(strcmp(measure, "L2") == 0) {
    while(attempts--) {
      int64_t offset = rand()%(L1D_CACHE_SIZE+L2_CACHE_SIZE);
      __asm__ volatile(
          "mfence\n\t"
          "mov %0, %%al\n\t"
          "mfence\n\t"
          :
          :"m"(d2[offset])
          );
    }
  }
  
  return 0;
}
  • コンパイルと実行
    コンパイルと実行は以下のコマンドで行いました。
    実行では--cpuオプションと taskset -cでCPUコアを固定し、--repeatオプションで10回分の算術平均を出力するようにしています。
    折角なので、命令数(instructions)とL1Dキャッシュロード数(L1-dcache-loads)も測定しています。
$ gcc -O3 -Wall code.c
$ perf stat -e cycles,instructions,L1-dcache-loads,L1-dcache-load-misses,\
--cpu 0 --repeat 10 taskset -c 0 ./a.out L1
$ perf stat -e cycles,instructions,L1-dcache-loads,L1-dcache-load-misses,\
--cpu 0 --repeat 10 taskset -c 0 ./a.out L2
  • 結果
    乱数確認の出力は省略します。
    データをon L1にした場合とそれ以上の場合とで、L1Dキャッシュミス数(L1-dcache-load-misses)が0.27% → 3.99%に増加しました。
    分母がコード全体の数なのでこの程度になるのだと思います。
$ perf stat -e cycles,instructions,L1-dcache-loads,L1-dcache-load-misses,\
--cpu 0 --repeat 10 taskset -c 0 ./a.out L1

 Performance counter stats for 'CPU(s) 0' (10 runs):

       11857147524      cycles                                                        ( +-  0.37% )
        7922673782      instructions              #    0.67  insn per cycle           ( +-  0.00% )
        2209211232      L1-dcache-loads                                               ( +-  0.00% )
           6053971      L1-dcache-load-misses     #    0.27% of all L1-dcache hits    ( +-  2.45% )
           1712144      L1-icache-load-misses                                         ( +-  0.64% )

           2.58984 +- 0.00953 seconds time elapsed  ( +-  0.37% )


$ perf stat -e cycles,instructions,L1-dcache-loads,L1-dcache-load-misses,\
--cpu 0 --repeat 10 taskset -c 0 ./a.out L2

 Performance counter stats for 'CPU(s) 0' (10 runs):

       13080667728      cycles                                                        ( +-  0.73% )
        7725468060      instructions              #    0.59  insn per cycle           ( +-  0.00% )
        2209998451      L1-dcache-loads                                               ( +-  0.00% )
          88094700      L1-dcache-load-misses     #    3.99% of all L1-dcache hits    ( +-  0.02% )
           1908817      L1-icache-load-misses                                         ( +-  1.26% )

            2.8802 +- 0.0296 seconds time elapsed  ( +-  1.03% )


  • L2キャッシュレイテンシの計算
    測定結果から、サイクル数とL1Dキャッシュミス数が測定できたので、差分からL2アクセスレイテンシは以下になります。
    一般的には12~cyclesということなので、この結果は大体正しいようです。
cycles : 13080667728 - 11857147524 = 1223520204
L1-dcache-load-misses : 88094700 - 6053971 = 82040729
latency = 1223520204/82040729 = 14.9 (cycles)

perfについては引き続き勉強していきたいと思います。

測定環境

  • CPU: Intel(R) Core(TM) i7-9700 CPU @ 3.00GHz
  • OS: Ubuntu-20.04
  • kernel: 5.4.0-91-generic
  • perf: 5.4.151
  • gcc: 11.1.0

参考・引用

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2