Linux で rdpmc
なんか昔はユーザ空間でrdpmc使えなかった気がするのだけど、使えるようになっていた。
(http://man7.org/linux/man-pages/man2/perf_event_open.2.html 3.4からと書いてある)
ちょっとループのIPCとりたかったのでInstructions Retiredとりたかったのだけど、ループが1000clkとかなので、readで読むのは難しかった。
が、
http://lxr.free-electrons.com/source/include/uapi/linux/perf_event.h#L359
なんか適当に見てたらこんな感じでrdpmc使えるよ、みたいなことが書いてあったので試したらできた。
#include <unistd.h> #include <sys/ioctl.h> #include <sys/types.h> #include <sys/mman.h> #include <x86intrin.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <linux/perf_event.h> #include <asm/unistd.h> static int perfctr_fd[1]; static int insns_pmc_index; static void *insns_pmc_addr; #define barrier() _mm_mfence() static int perf_event_open( struct perf_event_attr *hw_event, pid_t pid, int cpu, int group_fd, unsigned long flags ) { int ret; ret = syscall( __NR_perf_event_open, hw_event, pid, cpu, group_fd, flags ); return ret; } static void init_attr(struct perf_event_attr *pe, int config) { memset(pe, 0, sizeof(*pe)); pe->type = PERF_TYPE_HARDWARE; pe->size = sizeof(struct perf_event_attr); pe->config = config; } static long long read_insns(void) { struct perf_event_mmap_page *pc = (struct perf_event_mmap_page*)insns_pmc_addr; typedef unsigned int u32; typedef unsigned long long u64; typedef signed long long s64; u32 seq; u64 count; s64 pmc = 0; do { seq = pc->lock; barrier(); count = pc->offset; pmc = __rdpmc(pc->index - 1); barrier(); } while (pc->lock != seq); return count + pmc; } static void init_perf_control() { struct perf_event_attr pe; init_attr(&pe, PERF_COUNT_HW_INSTRUCTIONS); perfctr_fd[0] = perf_event_open(&pe, 0, -1, -1, 0); if (perfctr_fd[0] == 0) { perror("perf_event_open"); exit(1); } void *addr = mmap(NULL, 4096, PROT_READ, MAP_SHARED, perfctr_fd[0], 0); struct perf_event_mmap_page *pc = (struct perf_event_mmap_page*)addr; insns_pmc_index = pc->index - 1; insns_pmc_addr = addr; } int main() { init_perf_control(); long long a0, a1, t0, t1; a0 = read_insns(); asm("nop;nop;nop;"); asm("nop;nop;nop;"); asm("nop;nop;nop;"); asm("nop;nop;nop;"); a1 = read_insns(); printf("%lld\n", a1-a0); t0 = __rdtsc(); a0 = read_insns(); asm("nop;nop;nop;"); asm("nop;nop;nop;"); a1 = read_insns(); t1 = __rdtsc(); printf("%lld %lld\n", a1-a0, t1-t0); }
まず、perf_event_open でカウンタ初期化。
このあと、そのperf_event_openが返したfdをmmapすると、struct perf_event_mmap_page を指すポインタがとれる。これに入っている"index"をrdpmcで読むと、値がとれる。
ただ、単純にそれをrdpmcしてしまうと、スレッドがコア移動したときとかに値が狂うので、もう少しケアする必要がある。
do { seq = pc->lock; barrier(); count = pc->offset; pmc = __rdpmc(pc->index - 1); barrier(); } while (pc->lock != seq);
まず、lockを読む。カーネルがperf_event_mmap_pageを更新すると、このlockの値が変わるので、カーネルと変更が衝突していないかを判断するのに使う。
んで、コア移動した時とかサスペンドしたときの調整用に、offsetという値が入っているので、これとrdpmcで読んだ値をたす。
これでプロセス内で一貫した値がとれるようになる。スレッドは…どうなるんだっけ。まあ何も調べてない。わかったらまた書く。