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

问题描述

我正在尝试找到一个 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,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,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);
    err_assert(leadfd >= 0);

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

    init_perf_event_struct(&pe,ref_cycles_ev,0) >= 0);


    init_perf_event_struct(&pe,mem_load_retired_l1_hit,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,%[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],%[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);
    err_assert(addr != NULL);
    __builtin_memset(addr,TSIZE * PAGE_SIZE);
    return uint64_t(addr);
}

int
perf_event_open(struct perf_event_attr * hw_event,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,ev0,0) >= 0);

#if NEVENTS > 1
    init_perf_event_struct(&pe,ev1,0) >= 0);
#endif

    return leadfd;
}

void
start_perf_tracking(int leadfd) {
    ioctl(leadfd,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],%[ecount0]\n\t"
#if NEVENTS > 1
        "movl $1,%[ecount1]\n\t"
#endif
        SERIALIZER()
        DO_MEMORY_OP(prefetch_addr)
        SERIALIZER()
        "xorl %%ecx,%%ecx\n\t"
        "rdpmc\n\t"
        "subl %[ecount0],%%ecx\n\t"
        "rdpmc\n\t"
        "subl %[ecount1],%[ecount1]\n\t"
#endif
        "movl %[core_cycles_idx],%[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),"E0     : %d\n",ecount0);
    fprintf(stderr,"E1     : %d\n",ecount1);
    fprintf(stderr,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 的命中数,但可以从 SW_PREFETCH_ACCESS.T0 减去它们的总和获取 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 以及被忽略的 { {1}}。

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

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

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

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

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

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

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

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

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...