PMC计数软件预取是否命中一级缓存



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

icelake客户端:英特尔(R)酷睿(TM)i7-1065G7 CPU@1.30GHz

我正在尝试制作这种细粒度,即(注意应包括prefetcht0周围的lfence)

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和英特尔手册中的一些事件,但没有运气:

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缓存?(即我的测试不好)
  2. 如果没有。什么事件可以用来检测预取是否命中L1数据缓存

编辑: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: %sn", __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) "])nt"
#else
#define DO_MEMORY_OP(addr) "movl (%[" V_TO_STR(addr) "]), %%eaxnt"
#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(
"lfencent"
"movl %[core_cycles_idx], %%ecxnt"
"rdpmcnt"
"movl %%eax, %[cycles_to_detect]nt"
"xorl %%ecx, %%ecxnt"
"rdpmcnt"
"movl %%eax, %[prefetch_miss]nt"
"lfencent"
DO_MEMORY_OP(prefetch_addr)
"lfencent"
"xorl %%ecx, %%ecxnt"
"rdpmcnt"
"subl %[prefetch_miss], %%eaxnt"
"movl %%eax, %[prefetch_miss]nt"
"movl %[core_cycles_idx], %%ecxnt"
"rdpmcnt"
"subl %[cycles_to_detect], %%eaxnt"
"movl %%eax, %[cycles_to_detect]nt"
"lfencent"
: [ 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    : %dn", prefetch_miss);
fprintf(stderr, "Cycles : %dn", 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

并且没有CCD_ 12

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能够使其工作。非常感谢哈迪·布雷斯。值得注意的是,L1D_PEND_MISS.PENDING不起作用的原因可能与冰湖有关。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: %sn", __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() "lfencent"
#else
#define SERIALIZER()                                                           
"xorl %%ecx, %%ecxnt"                                                    
"xorl %%eax, %%eaxnt"                                                    
"cpuidnt"
#endif
#define DO_PREFETCH
#ifdef DO_PREFETCH
#define DO_MEMORY_OP(addr) "prefetcht0 (%[" V_TO_STR(addr) "])nt"
#else
#define DO_MEMORY_OP(addr) "movl (%[" V_TO_STR(addr) "]), %%eaxnt"
#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], %%ecxnt"
"rdpmcnt"
"movl %%eax, %[cycles_to_detect]nt"
"xorl %%ecx, %%ecxnt"
"rdpmcnt"
"movl %%eax, %[ecount0]nt"
#if NEVENTS > 1
"movl $1, %%ecxnt"
"rdpmcnt"
"movl %%eax, %[ecount1]nt"
#endif
SERIALIZER()
DO_MEMORY_OP(prefetch_addr)
SERIALIZER()
"xorl %%ecx, %%ecxnt"
"rdpmcnt"
"subl %[ecount0], %%eaxnt"
"movl %%eax, %[ecount0]nt"
#if NEVENTS > 1
"movl $1, %%ecxnt"
"rdpmcnt"
"subl %[ecount1], %%eaxnt"
"movl %%eax, %[ecount1]nt"
#endif
"movl %[core_cycles_idx], %%ecxnt"
"rdpmcnt"
"subl %[cycles_to_detect], %%eaxnt"
"movl %%eax, %[cycles_to_detect]nt"
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     : %dn", ecount0);
fprintf(stderr, "E1     : %dn", ecount1);
fprintf(stderr, "Cycles : %dn", 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

每个CCD_ 21指令被夹在CCD_ 22指令之间。这确保了对发生在两个rdpmc指令之间的任何事件以及仅这些事件进行计数。

CCD_ 24指令的预取操作可以被忽略或者被执行。如果执行了它,它可能会命中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中的预取操作未命中(导致向L2发送请求)并导致L1D中有效行被替换时,才会发生L1D.REPLACEMENT事件。通常,大多数填充都会导致替换,但该事件仍然不能用于区分在L1D中命中的prefetcht0、在分配给硬件预取的LFB中命中的prefetcht0和被忽略的prefetcht0

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

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

  • 首先,在内联程序集之前添加行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中的命中区分开,也可能不分开。这种情况可以通过在第一个CCD_ 54之前使用CCD_ 52而不是CCD_
  • 我不认为对可预取内存类型的预取会在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中被拒绝或命中,则这两个事件都不会发生。此外,上述所有情况将无法与L1D命中区分开来。

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

最新更新