perf-statによるPMCトレース

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

- [Perf Linux Profilerについて](#perf-linux-profilerについて)
- [perf-statによる計測の基本](#perf-statによる計測の基本)
- [実験:キャッシュアクセスレイテンシの測定](#実験キャッシュアクセスレイテンシの測定)

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


![Event Sources](https://www.brendangregg.com/perf_events/perf_events_map.png "Perf 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_paranoid```が```1```以下である必要があります。これは、ユーザ特権に応じてパフォーマンス監視を制限するキーで、デフォルトは```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キャッシュアクセスのレイテンシを計算してみます。
 
```C
// 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

## 参考・引用
- [perf-stat(1) — Linux manual page](https://man7.org/linux/man-pages/man1/perf-stat.1.html)
- [Brendan's site - perf Examples](https://www.brendangregg.com/perf.html)
- [stack overflow - Measureing Cache Latencies](https://stackoverflow.com/questions/21369381/measuring-cache-latencies)