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で読んだ値をたす。
これでプロセス内で一貫した値がとれるようになる。スレッドは…どうなるんだっけ。まあ何も調べてない。わかったらまた書く。