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
には、list
、stat
、report
などのサブコマンドがあり、それぞれのオプションも多数あります。機能の詳細はmanual pageなどにまとまっています。
今回はperf stat
(以下、perf-stat)でコード全体の統計情報を取得します。
図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キャッシュアクセスのレイテンシを計算してみます。
// 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