PMC 计算软件预取是否命中 L1 缓存

PMC to count if software prefetch hit L1 cache

我正在尝试找到一个 PMC(性能监控计数器),它将显示 prefetcht0 指令命中 L1 dcache(或未命中)的次数。

icelake-client: Intel(R) Core(TM) i7-1065G7 CPU @ 1.30GHz

我正在尝试制作这种精细的颗粒,即(注意应包括 lfence prefetcht0 左右)

    xorl %ecx, %ecx
    rdpmc
    movl %eax, %edi
    prefetcht0 (%rsi)
    rdpmc
    testl %eax, %edi
    // jump depending on if it was a miss or not

目标是检查预取是否命中 L1。如果没有执行一些准备好的代码,否则继续。

根据可用的情况,这似乎必须是一个错过的事件。

我已经尝试了一些事件 from libpfm4 和英特尔手册,但没有成功:

L1-DCACHE-LOAD-MISSES, emask=0x00, umask=0x10000
L1D.REPLACEMENT, emask=0x51, umask=0x1 
L2_RQSTS.SWPF_HIT, emask=0x24, umask=0xc8
L2_RQSTS.SWPF_MISS, emask=0x24, umask=0x28
LOAD_HIT_PREFETCH.SWPF, emask=0x01, umask=0x4c  (this very misleadingly is non-sw prefetch hits)

L1D.REPLACEMENTL1-DCACHE-LOAD-MISSES 是一种工作方式,如果我延迟 rdpmc 就可以工作,但如果它们一个接一个地出现,那充其量似乎也不可靠。其他的都是半身像。

问题:

  1. 这些是否应该用于检测预取是否命中 L1 dcache? (即我的测试很糟糕)
  2. 如果没有。什么事件可用于检测预取是否命中 L1 dcache?

编辑:MEM_LOAD_RETIRED.L1_HIT 似乎不适用于软件预取。

这是我用来做测试的代码:

#include <asm/unistd.h>
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <linux/perf_event.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <unistd.h>


#define HIT  0
#define MISS 1

#define TODO MISS


#define PAGE_SIZE 4096

// to force hit make TSIZE low
#define TSIZE     10000

#define err_assert(cond)                                                       \
    if (__builtin_expect(!(cond), 0)) {                                        \
        fprintf(stderr, "%d:%d: %s\n", __LINE__, errno, strerror(errno));      \
        exit(-1);                                                              \
    }


uint64_t
get_addr() {
    uint8_t * addr =
        (uint8_t *)mmap(NULL, TSIZE * PAGE_SIZE, PROT_READ | PROT_WRITE,
                        MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    err_assert(addr != NULL);


    for (uint32_t i = 0; i < TSIZE; ++i) {
        addr[i * PAGE_SIZE + (PAGE_SIZE - 1)] = 0;
        #if TODO == HIT
        addr[i * PAGE_SIZE] = 0;
        #endif
    }

    return uint64_t(addr);
}

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;
}

void
init_perf_event_struct(struct perf_event_attr * pe,
                       const uint32_t           type,
                       const uint64_t           ev_config,
                       int                      lead) {
    __builtin_memset(pe, 0, sizeof(struct perf_event_attr));

    pe->type           = type;
    pe->size           = sizeof(struct perf_event_attr);
    pe->config         = ev_config;
    pe->disabled       = !!lead;
    pe->exclude_kernel = 1;
    pe->exclude_hv     = 1;
}


/* Fixed Counters */
static constexpr uint32_t core_instruction_ev  = 0x003c;
static constexpr uint32_t core_instruction_idx = (1 << 30) + 0;

static constexpr uint32_t core_cycles_ev  = 0x00c0;
static constexpr uint32_t core_cycles_idx = (1 << 30) + 1;

static constexpr uint32_t ref_cycles_ev  = 0x0300;
static constexpr uint32_t ref_cycles_idx = (1 << 30) + 2;

/* programmable counters */
static constexpr uint32_t mem_load_retired_l1_hit  = 0x01d1;
static constexpr uint32_t mem_load_retired_l1_miss = 0x08d1;


int
init_perf_tracking() {
    struct perf_event_attr pe;

    init_perf_event_struct(&pe, PERF_TYPE_RAW, core_instruction_ev, 1);
    int leadfd = perf_event_open(&pe, 0, -1, -1, 0);
    err_assert(leadfd >= 0);

    init_perf_event_struct(&pe, PERF_TYPE_RAW, core_cycles_ev, 0);
    err_assert(perf_event_open(&pe, 0, -1, leadfd, 0) >= 0);

    init_perf_event_struct(&pe, PERF_TYPE_RAW, ref_cycles_ev, 0);
    err_assert(perf_event_open(&pe, 0, -1, leadfd, 0) >= 0);


    init_perf_event_struct(&pe, PERF_TYPE_RAW, mem_load_retired_l1_hit, 0);
    err_assert(perf_event_open(&pe, 0, -1, leadfd, 0) >= 0);

    return leadfd;
}

void
start_perf_tracking(int leadfd) {
    ioctl(leadfd, PERF_EVENT_IOC_RESET, 0);
    ioctl(leadfd, PERF_EVENT_IOC_ENABLE, 0);
}

#define _V_TO_STR(X) #X
#define V_TO_STR(X)  _V_TO_STR(X)

//#define DO_PREFETCH
#ifdef DO_PREFETCH
#define DO_MEMORY_OP(addr) "prefetcht0 (%[" V_TO_STR(addr) "])\n\t"
#else
#define DO_MEMORY_OP(addr) "movl (%[" V_TO_STR(addr) "]), %%eax\n\t"
#endif


int
main() {
    int fd = init_perf_tracking();
    start_perf_tracking(fd);

    uint64_t addr = get_addr();

    uint32_t prefetch_miss, cycles_to_detect;
    asm volatile(
        "lfence\n\t"
        "movl %[core_cycles_idx], %%ecx\n\t"
        "rdpmc\n\t"
        "movl %%eax, %[cycles_to_detect]\n\t"
        "xorl %%ecx, %%ecx\n\t"
        "rdpmc\n\t"
        "movl %%eax, %[prefetch_miss]\n\t"
        "lfence\n\t"
        DO_MEMORY_OP(prefetch_addr)
        "lfence\n\t"
        "xorl %%ecx, %%ecx\n\t"
        "rdpmc\n\t"
        "subl %[prefetch_miss], %%eax\n\t"
        "movl %%eax, %[prefetch_miss]\n\t"
        "movl %[core_cycles_idx], %%ecx\n\t"
        "rdpmc\n\t"
        "subl %[cycles_to_detect], %%eax\n\t"
        "movl %%eax, %[cycles_to_detect]\n\t"
        "lfence\n\t"
        : [ prefetch_miss ] "=&r"(prefetch_miss),
          [ cycles_to_detect ] "=&r"(cycles_to_detect)
        : [ prefetch_addr ] "r"(addr), [ core_cycles_idx ] "i"(core_cycles_idx)
        : "eax", "edx", "ecx");

    fprintf(stderr, "Hit    : %d\n", prefetch_miss);
    fprintf(stderr, "Cycles : %d\n", cycles_to_detect);
}

如果我定义 DO_PREFETCHMEM_LOAD_RETIRED.L1_HIT 的结果总是 1(似乎总是命中)。如果我注释掉 DO_PREFETCH 结果符合我的预期(当地址明显不在缓存中时报告未命中,当它明显报告命中时)。

DO_PREFETCH:

g++ -DDO_PREFETCH -O3 -march=native -mtune=native prefetch_hits.cc -o prefetch_hits
$> ./prefetch_hits
Hit    : 1
Cycles : 554

并且没有DO_PREFETCH

g++ -DDO_PREFETCH -O3 -march=native -mtune=native prefetch_hits.cc -o prefetch_hits
$> ./prefetch_hits
Hit    : 0
Cycles : 888

使用 L2_RQSTS.SWPF_HITL2_RQSTS.SWPF_MISS 可以让它工作。非常感谢 Hadi Brais。值得注意的是 L1D_PEND_MISS.PENDING 不起作用的原因可能与 Icelake 有关。 Hadi Brais 报告说它可以预测 Haswell 上的 L1D 缓存未命中。

为了尝试确定 L1_PEND_MISS.PENDINGMEM_LOAD_RETIRED.L1_HIT 为何不起作用,请发布我用于测试它们的确切代码:

#include <asm/unistd.h>
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <linux/perf_event.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <unistd.h>


#define HIT  0
#define MISS 1

#define TODO MISS


#define PAGE_SIZE 4096

#define TSIZE 1000

#define err_assert(cond)                                                       \
    if (__builtin_expect(!(cond), 0)) {                                        \
        fprintf(stderr, "%d:%d: %s\n", __LINE__, errno, strerror(errno));      \
        exit(-1);                                                              \
    }


uint64_t
get_addr() {
    uint8_t * addr =
        (uint8_t *)mmap(NULL, TSIZE * PAGE_SIZE, PROT_READ | PROT_WRITE,
                        MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    err_assert(addr != NULL);
    __builtin_memset(addr, -1, TSIZE * PAGE_SIZE);
    return uint64_t(addr);
}

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;
}

void
init_perf_event_struct(struct perf_event_attr * pe,
                       const uint32_t           type,
                       const uint64_t           ev_config,
                       int                      lead) {
    __builtin_memset(pe, 0, sizeof(struct perf_event_attr));

    pe->type           = type;
    pe->size           = sizeof(struct perf_event_attr);
    pe->config         = ev_config;
    pe->disabled       = !!lead;
    pe->exclude_kernel = 1;
    pe->exclude_hv     = 1;
}


/* Fixed Counters */
static constexpr uint32_t core_instruction_ev  = 0x003c;
static constexpr uint32_t core_instruction_idx = (1 << 30) + 0;

static constexpr uint32_t core_cycles_ev  = 0x00c0;
static constexpr uint32_t core_cycles_idx = (1 << 30) + 1;

static constexpr uint32_t ref_cycles_ev  = 0x0300;
static constexpr uint32_t ref_cycles_idx = (1 << 30) + 2;

/* programmable counters */
static constexpr uint32_t mem_load_retired_l1_hit  = 0x01d1;
static constexpr uint32_t mem_load_retired_l1_miss = 0x08d1;
static constexpr uint32_t l1d_pending              = 0x0148;
static constexpr uint32_t swpf_hit                 = 0xc824;
static constexpr uint32_t swpf_miss                = 0x2824;
static constexpr uint32_t ev0                      = l1d_pending;

#define NEVENTS 1
#if NEVENTS > 1
static constexpr uint32_t ev1 = swpf_miss;
#endif

int
init_perf_tracking() {
    struct perf_event_attr pe;

    init_perf_event_struct(&pe, PERF_TYPE_RAW, core_instruction_ev, 1);
    int leadfd = perf_event_open(&pe, 0, -1, -1, 0);
    err_assert(leadfd >= 0);

    init_perf_event_struct(&pe, PERF_TYPE_RAW, core_cycles_ev, 0);
    err_assert(perf_event_open(&pe, 0, -1, leadfd, 0) >= 0);

    init_perf_event_struct(&pe, PERF_TYPE_RAW, ref_cycles_ev, 0);
    err_assert(perf_event_open(&pe, 0, -1, leadfd, 0) >= 0);

    init_perf_event_struct(&pe, PERF_TYPE_RAW, ev0, 0);
    err_assert(perf_event_open(&pe, 0, -1, leadfd, 0) >= 0);

#if NEVENTS > 1
    init_perf_event_struct(&pe, PERF_TYPE_RAW, ev1, 0);
    err_assert(perf_event_open(&pe, 0, -1, leadfd, 0) >= 0);
#endif

    return leadfd;
}

void
start_perf_tracking(int leadfd) {
    ioctl(leadfd, PERF_EVENT_IOC_RESET, 0);
    ioctl(leadfd, PERF_EVENT_IOC_ENABLE, 0);
}

#define _V_TO_STR(X) #X
#define V_TO_STR(X)  _V_TO_STR(X)

//#define LFENCE
#ifdef LFENCE
#define SERIALIZER() "lfence\n\t"
#else
#define SERIALIZER()                                                           \
    "xorl %%ecx, %%ecx\n\t"                                                    \
    "xorl %%eax, %%eax\n\t"                                                    \
    "cpuid\n\t"

#endif

#define DO_PREFETCH

#ifdef DO_PREFETCH
#define DO_MEMORY_OP(addr) "prefetcht0 (%[" V_TO_STR(addr) "])\n\t"
#else
#define DO_MEMORY_OP(addr) "movl (%[" V_TO_STR(addr) "]), %%eax\n\t"
#endif


int
main() {
    int fd = init_perf_tracking();
    start_perf_tracking(fd);

    uint64_t addr = get_addr();

    // to ensure page in TLB
    *((volatile uint64_t *)(addr + (PAGE_SIZE - 8))) = 0;
    
#if TODO == HIT
    // loading from 0 offset to check cache miss / hit
    *((volatile uint64_t *)addr) = 0;
#endif

    uint32_t ecount0 = 0, ecount1 = 0, cycles_to_detect = 0;
    asm volatile(
        SERIALIZER()
        "movl %[core_cycles_idx], %%ecx\n\t"
        "rdpmc\n\t"
        "movl %%eax, %[cycles_to_detect]\n\t"
        "xorl %%ecx, %%ecx\n\t"
        "rdpmc\n\t"
        "movl %%eax, %[ecount0]\n\t"
#if NEVENTS > 1
        "movl , %%ecx\n\t"
        "rdpmc\n\t"
        "movl %%eax, %[ecount1]\n\t"
#endif
        SERIALIZER()
        DO_MEMORY_OP(prefetch_addr)
        SERIALIZER()
        "xorl %%ecx, %%ecx\n\t"
        "rdpmc\n\t"
        "subl %[ecount0], %%eax\n\t"
        "movl %%eax, %[ecount0]\n\t"
#if NEVENTS > 1
        "movl , %%ecx\n\t"
        "rdpmc\n\t"
        "subl %[ecount1], %%eax\n\t"
        "movl %%eax, %[ecount1]\n\t"
#endif
        "movl %[core_cycles_idx], %%ecx\n\t"
        "rdpmc\n\t"
        "subl %[cycles_to_detect], %%eax\n\t"
        "movl %%eax, %[cycles_to_detect]\n\t"
        SERIALIZER()
        : [ ecount0 ] "=&r"(ecount0),
#if NEVENTS > 1
          [ ecount1 ] "=&r"(ecount1),
#endif
          [ cycles_to_detect ] "=&r"(cycles_to_detect)
        : [ prefetch_addr ] "r"(addr), [ core_cycles_idx ] "i"(core_cycles_idx)
        : "eax", "edx", "ecx");

    fprintf(stderr, "E0     : %d\n", ecount0);
    fprintf(stderr, "E1     : %d\n", ecount1);
    fprintf(stderr, "Cycles : %d\n", cycles_to_detect);
}

rdpmc 未按程序顺序排列其之前或之后可能发生的事件。需要一个完全序列化的指令,例如 cpuid,以获得关于 prefetcht0 的所需排序保证。代码应该如下:

xor  %eax, %eax         # CPUID leaf eax=0 should be fast.  Doing this before each CPUID might be a good idea, but omitted for clarity
cpuid
xorl %ecx, %ecx
rdpmc
movl %eax, %edi         # save RDPMC result before CPUID overwrites EAX..EDX
cpuid
prefetcht0 (%rsi)
cpuid
xorl %ecx, %ecx
rdpmc
testl %eax, %edi        # CPUID doesn't affect FLAGS
cpuid

每个 rdpmc 指令都夹在 cpuid 指令之间。这确保计算两个 rdpmc 指令之间发生的任何事件,并且只计算这些事件。

prefetcht0 指令的预取操作可能会被忽略或执行。如果它被执行,它可能会命中在 L1D 中处于有效状态的缓存行,也可能不会。这些都是必须考虑的情况。

L2_RQSTS.SWPF_HITL2_RQSTS.SWPF_MISS的总和不能用于计算或推导L1D中prefetcht0的命中数,但可以从[=21=中减去它们的总和] 以获得 L1D 中 prefetcht0 命中数的上限。使用上面显示的正确序列化序列,我认为唯一未忽略的 prefetcht0 不会在 L1D 中命中并且不计入总和 SWPF_HIT+SWPF_MISS 的情况是软件预取操作命中分配给硬件预取的 LFB。

L1-DCACHE-LOAD-MISSES 只是 L1D.REPLACEMENT 的别称。您为 L1-DCACHE-LOAD-MISSES 显示的事件代码和 umask 不正确。 L1D.REPLACEMENT 事件仅在预取操作在 L1D 中未命中(这导致将请求发送到 L2)并导致 L1D 中的有效行被替换时才会发生。通常大多数填充会导致替换,但事件仍然不能用于区分命中 L1D 的 prefetcht0、命中分配给硬件预取的 LFB 的 prefetcht0 和被忽略的 prefetcht0.

事件 LOAD_HIT_PREFETCH.SWPF 在分配给软件预取的 LFB 中遇到需求负载时发生。这显然在这里没有用。

事件L1D_PEND_MISS.PENDING(事件=0x48,umask=0x01)应该有效。根据文档,此事件使计数器增加每个周期未决 L1D 未命中的数量。我认为它适用于需求加载和预取。这实际上是一个近似值,因此即使未决 L1D 未命中数为零,它也可能会计算在内。但我认为它仍然可以通过以下步骤以非常高的置信度确定 L1D 中是否遗漏了一个 prefetcht0

  • 首先,在内联汇编之前添加 uint64_t value = *(volatile uint64_t*)addr; 行。这是为了将要预取的行在 L1D 中的概率提高到接近 100%。
  • 其次,测量极有可能命中L1D的prefetcht0的最小增量L1D_PEND_MISS.PENDING
  • 运行 多次实验以建立高信心,即最小增量高度稳定,几乎在每个 运行.
  • 中观察到相同的精确值。
  • 注释掉第一步中添加的行,以便 prefetcht0 未命中并检查事件计数变化是否总是或几乎总是大于之前测量的最小增量。

到目前为止,我只关心区分在 L1D 中命中的预取和在 L1D 和 LFB 中均未命中的非忽略预取。现在我将考虑其余的情况:

  • 如果预取导致页面错误,或者目标缓存行的内存类型为 WC 或 UC,则忽略预取。不知道是否可以通过L1D_PEND_MISS.PENDING事件来区分命中和本例。您可以 运行 试验预取指令的目标地址位于没有有效映射的虚拟页面或映射到内核页面的位置。检查事件计数的变化是否具有高概率的唯一性。
  • 如果没有可用的 LFB,则忽略预取。这种情况可以通过关闭同级逻辑核心并在第一个 rdpmc.
  • 之前使用 cpuid 而不是 lfence 来消除
  • 如果预取命中分配给 RFO、ItoM 或硬件预取请求的 LFB,则预取实际上是冗余的。对于所有这些类型的请求,L1D_PEND_MISS.PENDING 计数的变化可能会或不会与 L1D 中的命中区分开来。这种情况可以通过在第一个 rdpmc 之前使用 cpuid 而不是 lfence 并关闭两个 L1D 硬件预取器来消除。
  • 我认为预取到可预取内存类型不会在 WCB 中命中,因为更改位置的内存类型是完全序列化操作,所以这种情况不是问题。

使用 L1D_PEND_MISS.PENDING 而不是总和 SWPF_HIT+SWPF_MISS 的一个明显优势是事件数量较少。另一个优点是 L1D_PEND_MISS.PENDING 在一些较早的微体系结构上受支持。而且,如上所述,它可以更强大。它适用于我的 Haswell,阈值为 69-70 个周期。

如果L1D_PEND_MISS.PENDING不同情况下的事件变化不能区分,那么可以用SWPF_HIT+SWPF_MISS的和。这两个事件发生在 L2 上,因此它们仅告诉您 L1D 中是否错过了预取以及 L2 是否发送并接受了请求。如果请求在 L2 的 SQ 中被拒绝或命中,则可能发生两个事件中的none。此外,所有上述情况都无法与 L1D 命中区分开来。

对于正常的需求负载,您可以使用 MEM_LOAD_RETIRED.L1_HIT。如果负载命中 L1D,则会出现单个 L1_HIT。否则,在任何其他情况下,没有L1_HIT事件发生,假设两个rdpmc之间没有其他指令,例如cpuid,可以产生L1_HIT事件。您必须验证 cpuid 不会生成 L1_HIT 事件。不要忘记只计算用户模式事件,因为任何两条指令之间都可能发生中断,并且中断处理程序可能会在内核模式下生成一个或多个 L1_HIT 事件。虽然这不太可能,但如果您想 100% 确定,还要检查中断的发生本身是否会生成 L1_HIT 个事件。