x86-64 - 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.REPLACEMENT
和L1-DCACHE-LOAD-MISSES
一种作品,如果我延迟它会起作用,rdpmc
但如果它们一个接一个,它似乎充其量是不可靠的。其他的都是完整的半身像。
问题:
- 这些是否可以用于检测预取是否命中 L1 dcache?(即我的测试很糟糕)
- 如果不。什么事件可用于检测预取是否命中 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.PENDING
,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
#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);
}
解决方案
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
不能用于计算或导出prefetcht0
L1D 中的命中数,但可以从中减去它们的总和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 中的有效行被替换时,才会发生该事件。通常大多数填充会导致替换,但该事件仍然不能用于区分在prefetcht0
L1Dprefetcht0
中命中的 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.PENDING
a的最小增量。prefetcht0
- 多次运行实验以建立高度的信心,即最小增量高度稳定,几乎每次运行都观察到相同的精确值。
- 注释掉第一步中添加的行,以便
prefetcht0
未命中并检查事件计数变化是否总是或几乎总是大于先前测量的最小增量。
到目前为止,我只关心区分在 L1D 中命中的预取和在 L1D 和 LFB 中都未命中的未忽略预取。现在我将考虑其余的情况:
- 如果预取导致页面错误或目标缓存行的内存类型是 WC 或 UC,则忽略预取。我不知道这个
L1D_PEND_MISS.PENDING
事件是否可以用来区分命中和这种情况。您可以在预取指令的目标地址位于没有有效映射的虚拟页面或映射到内核页面的情况下运行实验。检查事件计数的变化是否具有很高的概率是唯一的。 - 如果没有 LFB 可用,则忽略预取。这种情况可以通过关闭同级逻辑核心并在第一个之前使用
cpuid
而不是使用来消除。lfence
rdpmc
- 如果预取在为 RFO、ItoM 或硬件预取请求分配的 LFB 中命中,则预取实际上是冗余的。对于所有这些类型的请求,
L1D_PEND_MISS.PENDING
计数的变化可能与 L1D 中的命中有区别,也可能没有区别。这种情况可以通过在两个 L1D 硬件预取器的第一个和翻转之前使用cpuid
代替。lfence
rdpmc
- 我认为对可预取内存类型的预取不会在 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
发生事件,假设两个rdpmc
s 之间没有其他指令,例如cpuid
,可以生成L1_HIT
事件。您必须验证cpuid
不会生成L1_HIT
事件。不要忘记只计算用户模式事件,因为任何两条指令之间都可能发生中断,并且中断处理程序可能会L1_HIT
在内核模式下生成一个或多个事件。虽然这不太可能,但如果您想 100% 确定,还要检查中断的发生本身是否会产生L1_HIT
事件。
推荐阅读
- javascript - 如何在 Slick carousel 上制作黑色背景过渡?
- c# - Visual Studio 找不到 Microsoft.AspNetCore.DataProtection 的 PersistKeysToRedis 方法
- mysql - MySQL:仅选择仅具有某些值的组
- url-masking - 是否可以使用谷歌域来转发用端口屏蔽的子域并将其屏蔽?
- android - Android - ExpandableListView 内容在屏幕底部被截断
- r - 在 `dplyr` 的 `mutate()` 中应用函数
- angular - 在页面加载时加载 MDBootstrap 模式
- grails - 带有 SAML 的 Grails 3 - 身份验证失败
- h2 - 在 H2 表名中使用大括号
- image - Xamarin 图像数据在流副本上无效