IntelのCPUではハードウェアイベントを計測できるPMC(Performance Monitoring Counter)という仕組みがあります。
PMCを使用して、イベントを計測するためには最低3つの手順でMSR(モデル固有レジスタ)に対してデータを読み書きしなければなりません。
- IA32_PERF_GLOBAL_CTRLを設定
- IA32_PERFEVTSELxを設定
- IA32_PMCxに対して読み込みをかける
IA32_PERF_GLOBAL_CTRLではIA32_PMCxの有効ビットをONにします。IA32_PERFEVTSELxではEventNumやUnitMaskなどを設定します。以上を正しく設定すると、IA32_PMCxからRDMSR命令またはRDPMC命令で設定したイベントのデータを読み取ることができます。
IA32_PERFEVETSELとIA32_PMCは複数存在し、それらは一対になっているのでIA32_PERFEVTSEL0で設定した内容はIA32_PMC0から読みととることになります。詳しい設定方法はIntelのドキュメント*1を参考にしてください。
PMCを扱うにあたって、いくつか課題があります。
- モデル固有レジスタにはスコープがあり、必ずしも論理CPUごとにレジスタを設定すればいいとは限らない
- システムの論理CPU数、CPUのモデルに依存する設定内容がある
- 一定時間(例えば数分間)計測して統計をとりたいことがある
などがそうです。というわけで、上記の要件を満たすようなライブラリを作ってみました。
API構成は以下のようになっています。
msr.h
#include <stdbool.h>
#include "bitops.h"
union IA32_PERFEVTSELx{
struct {
unsigned int EvtSel:8;
unsigned int UMASK:8;
unsigned int USER:1;
unsigned int OS:1;
unsigned int E:1;
unsigned int PC:1;
unsigned int INT:1;
unsigned int ANY:1; /* Facility-2だとここはreserved */
unsigned int EN:1;
unsigned int INV:1;
unsigned int CounterMask:8;
unsigned int RV2:32;
} split;
u64 full;
};
union UNCORE_PERFEVTSELx {
struct {
unsigned int EvtSel:8;
unsigned int UMASK:8;
unsigned int RV1:1;
unsigned int OCC_CTR_RST:1;
unsigned int E:1;
unsigned int RV2:1;
unsigned int PMI:1;
unsigned int RV3:1;
unsigned int EN:1;
unsigned int INV:1;
unsigned int CounterMask:8;
unsigned int RV4:32;
} split;
u64 full;
};
enum msr_scope{
thread,
core,
package
};
#define STR_MAX_TAG 64
struct msr_handle{
char tag[STR_MAX_TAG];
enum msr_scope scope;
off_t addr; /* レジスタのアドレス(get_msr()の引数になる) */
bool active; /* レジスタが使用可能になったらtrueにする */
u64 *flat_records; /* バッファ */
bool (*pre_closure)(int handle_id, u64 *cpu_val); /* バッファに格納する前に生データに対して行う処理 */
};
typedef struct msr_handle MHANDLE;
MHANDLE *alloc_handle(void);
/* 計測用関数 */
bool read_msr(void);
/* ハンドル有効化関数 */
bool activate_handle(MHANDLE *handle, const char *tag, enum msr_scope scope,
unsigned int addr, bool (*pre_closure)(int handle_id, u64 *cpu_val));
/* GLOBAL_CTRLの設定関数 */
int setup_PERF_GLOBAL_CTRL(void);
int setup_UNCORE_PERF_GLOBAL_CTRL(void);
/* PERFEVTSELxの設定関数 簡易版と詳細版 */
void setup_IA32_PERFEVTSEL_quickly(unsigned int sel, unsigned int umask, unsigned int event);
void setup_UNCORE_PERFEVTSEL_quickly(unsigned int sel, unsigned int umask, unsigned int event);
void setup_IA32_PERFEVTSEL(unsigned int addr, union IA32_PERFEVTSELx *reg);
void setup_UNCORE_PERFEVTSEL(unsigned int addr, union UNCORE_PERFEVTSELx *reg);
/* 初期化、終了関数 */
bool init_handle_controller(int max_records, int nr_handles);
void term_handle_controller(void *arg);
MSRを統一的に扱うために、MHANDLEというデータ構造を定義しました。イメージとしてはfopen(3)などでつかうFILE構造体みたいな感じです。
ただ、前提条件がありまして、msr.koをロードしていること、bitopsライブラリが使用可能であることの2つを満たしている必要があります。
PMCを使用してデータを読むときには
- init_handle_controller()
- setup_PERF_GLOBAL_CTRL()
- setup_IA32_PERFEVTSEL_quickly()
- alloc_handle()
- activate_handle()
- read_msr()
- term_handle_controller()
のような流れになります。MHANDLEを複数allocしても、read_msr()を呼び出せば、一気に設定してあるイベントを読み込み、バッファに格納してくれます。そして、term_handle_controller()を呼び出すとそれまでバッファに格納されていたデータがcsv形式で出力されます。
文章で書いてもイメージがつきにくいので、サンプルのプログラムを作成しました。
#include <stdio.h>
#include <unistd.h> /* sleep(3) */
#include "msr.h"
#include "msr_address.h"
static FILE *tmp_fp[2];
/*
前回のデータとの差分を取る関数 ライブラリ側で呼び出される
msr_handleに格納される関数(scope==thread or core用)
@handle_id mh_ctl.handles[]の添字
@val MSRを計測した生データ
return true/false
*/
bool sub_record_multi(int handle_id, u64 *val)
{
int nr_cpus = sysconf(_SC_NPROCESSORS_CONF);
int skip = 0;
u64 val_last[nr_cpus];
int i;
int num;
/*-- tmp_fpはcloseすると削除されるので、openとcloseはalloc,freeで行うこととする --*/
fseek(tmp_fp[handle_id], 0, SEEK_SET);
/* 過去のvalをtmp_fpから読み込む */
if((num = fread(val_last, sizeof(u64), nr_cpus, tmp_fp[handle_id])) != nr_cpus){
skip = 1;
}
fseek(tmp_fp[handle_id], 0, SEEK_SET);
/* 現在のcpu_valを書き込む */
fwrite(val, sizeof(u64), nr_cpus, tmp_fp[handle_id]);
fseek(tmp_fp[handle_id], 0, SEEK_SET);
for(i = 0; i < nr_cpus; i ++){
/* MSRの回数は増えることはあっても減ることはないのでここは絶対0以上 */
val[i] -= val_last[i];
}
if(skip){
return false;
}
else{
return true;
}
}
#define USE_NR_MSR 3 /* いくつのMSRを使ってイベントを計測するか */
int main(int argc, char *argv[])
{
int i, nr_ia32_pmcs;
MHANDLE *handles[USE_NR_MSR];
enum msr_scope scope = thread;
union IA32_PERFEVTSELx reg;
reg.full = 0;
/* tempファイルをオープン */
for(i = 0; i < USE_NR_MSR; i++){
tmp_fp[i] = tmpfile();
}
/* PerfGlobalCtrlレジスタを設定 */
nr_ia32_pmcs = setup_PERF_GLOBAL_CTRL();
printf("%d nr_ia32_pmcs registered.\n", nr_ia32_pmcs);
init_handle_controller(100, USE_NR_MSR); /* 100回、USE_NR_MSR個のMSRを使って計測する。という指定 */
/* PERFEVENTSELの設定 */
reg.split.EvtSel = EVENT_LONGEST_CACHE_LAT;
reg.split.UMASK = UMASK_LONGEST_CACHE_LAT_MISS;
reg.split.USER = 1;
reg.split.EN = 1;
setup_IA32_PERFEVTSEL(IA32_PERFEVENTSEL0, ®);
reg.split.USER = 0;
reg.split.OS = 1;
setup_IA32_PERFEVTSEL(IA32_PERFEVENTSEL1, ®);
reg.split.USER = 1;
reg.split.OS = 1;
setup_IA32_PERFEVTSEL(IA32_PERFEVENTSEL2, ®);
//setup_IA32_PERFEVTSEL_quickly(IA32_PERFEVENTSEL0, UMASK_LONGEST_CACHE_LAT_MISS, EVENT_LONGEST_CACHE_LAT);
//setup_IA32_PERFEVTSEL_quickly(IA32_PERFEVENTSEL1, UMASK_LONGEST_CACHE_LAT_REFERENCE, EVENT_LONGEST_CACHE_LAT);
for(i = 0; i < USE_NR_MSR; i++){
handles[i] = alloc_handle();
}
activate_handle(handles[0], "LONGEST_LAT_CACHE.MISS USER only", scope, IA32_PMC0, sub_record_multi);
activate_handle(handles[1], "LONGEST_LAT_CACHE.MISS OS only", scope, IA32_PMC1, sub_record_multi);
activate_handle(handles[2], "LONGEST_LAT_CACHE.MISS both ring", scope, IA32_PMC2, sub_record_multi);
while(1){
sleep(1);
if(read_msr() == false){ /* MAX_RECORDS以上計測した */
puts("time over");
break;
}
}
/* 後始末 */
term_handle_controller(NULL);
for(i = 0; i < USE_NR_MSR; i++){
fclose(tmp_fp[i]);
}
return 0;
}
今回はring3とring0を別イベントとして扱いたかったので、msr.hで定義されている共用体を使ってIA32_PERFEVTSELxを設定しています。
#IA32_PERFEVTSELxのアドレス、EventNumとUnitMaskの3つだけで設定できる簡易版の関数も実装しました。
activate_handle()の第5引数にはMSRから取得した生データをバッファに格納する前に加工する関数を指定できるようにしています。ここで指定した関数はMHANDLEにpre_closureというシンボルで登録されます。
bool (*pre_closure)(int handle_id, u64 *cpu_val);
handle_idはMHANDLEの識別子で、cpu_valはMSRから取得した直後のデータが入った配列になります。sample.cでは前回との差分を計算する関数をpre_closureに登録しています。
今回はL3キャッシュのミス回数を『ring3だけ、ring0だけ、両方』の3種類のPMCを使って、1秒ごとに100回計測してみました。その結果のCSVファイルをExcelでグラフ化すると以下のようになります。
環境:Linux debian 2.6.39-2-amd64 [debian wheezy]
:Intel Corei7 2600 [4C/8T SandyBridge]
計測中は特に負荷はかけていないのですが、カーネルのスケジューリンググループでまとめられているCPU#0とCPU#4のグラフが連動していることが分かります。こういうのが楽にできるとOSのパフォーマンスが目に見えていいですね。
ソースはgithubに置いてあります。
