首页 > 解决方案 > PMC 计算软件预取是否命中 L1 缓存

问题描述

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

icelake 客户端: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。如果没有执行一些准备好的代码,否则继续。

仅基于可用的内容,它似乎必须是一个错过事件。

我尝试了libpfm4和 intel 手册中的一些事件,但没有成功:

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_PREFETCH结果定义为MEM_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_HIT并且L2_RQSTS.SWPF_MISS能够让它工作。非常感谢哈迪·布雷斯。值得注意的是,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 $1, %%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 $1, %%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);
}

标签: x86-64intelperformancecountermemory-barriersintel-pmu

解决方案


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_HIT和的总和L2_RQSTS.SWPF_MISS不能用于计算或导出prefetcht0L1D 中的命中数,但可以从中减去它们的总和SW_PREFETCH_ACCESS.T0以获得 L1D 中命中数的上限prefetcht0。使用上面显示的正确序列化序列,我认为唯一prefetcht0没有在 L1D 中命中且不计入总和SWPF_HIT+SWPF_MISS的情况是软件预取操作在分配给硬件预取的 LFB 中命中。

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

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

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

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

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

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

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

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

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


推荐阅读