Noa*_*oah 5 x86-64 intel cpu-architecture memcpy micro-optimization
我得到了错误的结果,因为我在测量时没有包括这里讨论的预取触发事件。话虽如此,AFAIKrep movsb与临时存储相比,我只看到 RFO 请求减少,memcpy因为在加载时预取更好,而没有对存储进行预取。不是因为 RFO 请求针对完整缓存行存储进行了优化。这种有意义的,因为我们没有看到RFO请求优化掉了vmovdqa一个zmm寄存器,我们预计如果真的在那里为整个缓存线存储情况。话虽如此,存储上缺乏预取和非临时写入的缺乏使得很难看出如何rep movsb具有合理的性能。
编辑:RFO 可能来自rep movsb不同的请求vmovdqa,因为rep movsb它可能不请求数据,只需在独占状态下取行即可。对于有收银机的商店,情况也可能如此zmm。但是,我没有看到任何性能指标来测试这一点。有谁知道吗?
rep movsb的memcpy作为相比,memcpy与实现的vmovdqa?rep movsb的memcpy作为相比,memcpy与实现vmovdqa两个单独的问题,因为我相信我应该看到 RFO 请求减少了rep movsb,但如果不是这种情况,我是否也应该看到增加?
CPU - Icelake: Intel(R) Core(TM) i7-1065G7 CPU @ 1.30GHz
我试图在使用不同的方法时测试 RFO 请求的数量,memcpy包括:
vmovdqavmovntdqrep movsb并且无法看到使用rep movsb. 事实上,我看到的 RFO 请求rep movsb比 Temporal Stores 多。鉴于共识理解似乎是 ivybridge 和 newrep movsb能够避免 RFO 请求,从而节省内存带宽,这是违反直觉的:
当发出 rep movs 指令时,CPU 知道要传输已知大小的整个块。这可以帮助它以离散指令无法实现的方式优化操作,例如:
- 当知道整个缓存行将被覆盖时避免 RFO 请求。
请注意,在 Ivybridge 和 Haswell 上,如果缓冲区足够大以适合 MLC,您可以使用 rep movsb 击败 movntdqa;movntdqa 导致对 LLC 的 RFO,rep movsb 没有
我编写了一个简单的测试程序来验证这一点,但无法这样做。
#include <assert.h>
#include <errno.h>
#include <immintrin.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#define BENCH_ATTR __attribute__((noinline, noclone, aligned(4096)))
#define TEMPORAL 0
#define NON_TEMPORAL 1
#define REP_MOVSB 2
#define NONE_OF_THE_ABOVE 3
#define TODO 1
#if TODO == NON_TEMPORAL
#define store(x, y) _mm256_stream_si256((__m256i *)(x), y)
#else
#define store(x, y) _mm256_store_si256((__m256i *)(x), y)
#endif
#define load(x) _mm256_load_si256((__m256i *)(x))
void *
mmapw(uint64_t sz) {
void * p = mmap(NULL, sz, PROT_READ | PROT_WRITE,
MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
assert(p != NULL);
return p;
}
void BENCH_ATTR
bench() {
uint64_t len = 64UL * (1UL << 22);
uint64_t len_alloc = len;
char * dst_alloc = (char *)mmapw(len);
char * src_alloc = (char *)mmapw(len);
for (uint64_t i = 0; i < len; i += 4096) {
// page in before testing. perf metrics appear to still come through
dst_alloc[i] = 0;
src_alloc[i] = 0;
}
uint64_t dst = (uint64_t)dst_alloc;
uint64_t src = (uint64_t)src_alloc;
uint64_t dst_end = dst + len;
asm volatile("lfence" : : : "memory");
#if TODO == REP_MOVSB
// test rep movsb
asm volatile("rep movsb" : "+D"(dst), "+S"(src), "+c"(len) : : "memory");
#elif TODO == TEMPORAL || TODO == NON_TEMPORAL
// test vmovtndq or vmovdqa
for (; dst < dst_end;) {
__m256i lo = load(src);
__m256i hi = load(src + 32);
store(dst, lo);
store(dst + 32, hi);
dst += 64;
src += 64;
}
#endif
asm volatile("lfence\n\tmfence" : : : "memory");
assert(!munmap(dst_alloc, len_alloc));
assert(!munmap(src_alloc, len_alloc));
}
int
main(int argc, char ** argv) {
bench();
}
Run Code Online (Sandbox Code Playgroud)
gcc -O3 -march=native -mtune=native rfo_test.c -o rfo_test
Run Code Online (Sandbox Code Playgroud)
perf stat -e cpu-cycles -e l2_rqsts.all_rfo -e offcore_requests_outstanding.cycles_with_demand_rfo -e offcore_requests.demand_rfo ./rfo_test
Run Code Online (Sandbox Code Playgroud)
注意:edit2中噪声较小的数据
583,912,867 cpu-cycles
9,352,817 l2_rqsts.all_rfo
188,343,479 offcore_requests_outstanding.cycles_with_demand_rfo
11,560,370 offcore_requests.demand_rfo
0.166557783 seconds time elapsed
0.044670000 seconds user
0.121828000 seconds sys
Run Code Online (Sandbox Code Playgroud)
560,933,296 cpu-cycles
7,428,210 l2_rqsts.all_rfo
123,174,665 offcore_requests_outstanding.cycles_with_demand_rfo
8,402,627 offcore_requests.demand_rfo
0.156790873 seconds time elapsed
0.032157000 seconds user
0.124608000 seconds sys
Run Code Online (Sandbox Code Playgroud)
566,898,220 cpu-cycles
11,626,162 l2_rqsts.all_rfo
178,043,659 offcore_requests_outstanding.cycles_with_demand_rfo
12,611,324 offcore_requests.demand_rfo
0.163038739 seconds time elapsed
0.040749000 seconds user
0.122248000 seconds sys
Run Code Online (Sandbox Code Playgroud)
521,061,304 cpu-cycles
7,527,122 l2_rqsts.all_rfo
123,132,321 offcore_requests_outstanding.cycles_with_demand_rfo
8,426,613 offcore_requests.demand_rfo
0.139873929 seconds time elapsed
0.007991000 seconds user
0.131854000 seconds sys
Run Code Online (Sandbox Code Playgroud)
只有安装但没有基线RFO请求memcpy是在TODO = NONE_OF_THE_ABOVE与7527122个RFO请求。
通过TODO = TEMPORAL(使用vmovdqa)我们可以看到9,352,817 个RFO 请求。这比TODO = REP_MOVSB(using rep movsb) 有11,626,162 个RFO 请求要低。使用rep movsb比使用临时存储多约 200 万个 RFO 请求。我能够看到避免 RFO 请求的唯一情况是TODO = NON_TEMPORAL(using vmovntdq),它有7,428,210 个RFO 请求,与表明没有来自memcpy自身的基线大致相同。
我为 memcpy 尝试了不同的大小,认为我可能需要减少/增加大小rep movsb以进行优化,但我一直看到相同的一般结果。对于我测试的所有尺寸,我看到 RFO 请求的数量按以下顺序NON_TEMPORAL< TEMPORAL< REP_MOVSB。
编辑:@PeterCordes 能够在 Skylake 上重现结果
我不认为这是一个Icelake具体的东西作为唯一的变化我可以在中找到英特尔手册上rep movsb的Icelake是:
从基于 Ice Lake Client 微架构的处理器开始,REP MOVSB 短操作的性能得到增强。增强适用于 1 到 128 个字节之间的字符串长度。CPUID 特性标志列举了对 fast-short REP MOVSB 的支持:CPUID [EAX=7H, ECX=0H).EDX.FAST_SHORT_REP_MOVSB[bit 4] = 1。REP STOS 性能没有变化。
鉴于len远高于 128 ,这不应该在我使用的测试程序中发挥作用。
我没有看到任何问题,但这是一个非常令人惊讶的结果。至少验证了编译器没有优化这里的测试
编辑:修复了使用的构建指令G++而不是GCC文件后缀从.c到.cc
编辑2:
回到 C 和 GCC。
perf stat --all-user -e cpu-cycles -e l2_rqsts.all_rfo -e offcore_requests_outstanding.cycles_with_demand_rfo -e offcore_requests.demand_rfo ./rfo_test
Run Code Online (Sandbox Code Playgroud)
具有更好性能配方的数字(趋势相同但噪音更小):
161,214,341 cpu-cycles
1,984,998 l2_rqsts.all_rfo
61,238,129 offcore_requests_outstanding.cycles_with_demand_rfo
3,161,504 offcore_requests.demand_rfo
0.169413413 seconds time elapsed
0.044371000 seconds user
0.125045000 seconds sys
Run Code Online (Sandbox Code Playgroud)
142,689,742 cpu-cycles
3,106 l2_rqsts.all_rfo
4,581 offcore_requests_outstanding.cycles_with_demand_rfo
30 offcore_requests.demand_rfo
0.166300952 seconds time elapsed
0.032462000 seconds user
0.133907000 seconds sys
Run Code Online (Sandbox Code Playgroud)
150,630,752 cpu-cycles
4,194,202 l2_rqsts.all_rfo
54,764,929 offcore_requests_outstanding.cycles_with_demand_rfo
4,194,016 offcore_requests.demand_rfo
0.166844489 seconds time elapsed
0.036620000 seconds user
0.130205000 seconds sys
Run Code Online (Sandbox Code Playgroud)
89,611,571 cpu-cycles
321 l2_rqsts.all_rfo
3,936 offcore_requests_outstanding.cycles_with_demand_rfo
19 offcore_requests.demand_rfo
0.142347046 seconds time elapsed
0.016264000 seconds user
0.126046000 seconds sys
Run Code Online (Sandbox Code Playgroud)
Edit3:这可能与隐藏 L2 Prefetcher 触发的 RFO 事件有关
我使用了 @BeeOnRope 制作的 pref 配方,其中包括由 L2 Prefetcher 启动的 RFO 事件:
perf stat --all-user -e cpu/event=0x24,umask=0xff,name=l2_rqsts_references/,cpu/event=0x24,umask=0xf2,name=l2_rqsts_all_rfo/,cpu/event=0x24,umask=0xd2,name=l2_rqsts_rfo_hit/,cpu/event=0x24,umask=0x32,name=l2_rqsts_rfo_miss/ ./rfo_test
Run Code Online (Sandbox Code Playgroud)
没有 L2 预取事件的等效性能配方:
perf stat --all-user -e cpu/event=0x24,umask=0xef,name=l2_rqsts_references/,cpu/event=0x24,umask=0xe2,name=l2_rqsts_all_rfo/,cpu/event=0x24,umask=0xc2,name=l2_rqsts_rfo_hit/,cpu/event=0x24,umask=0x22,name=l2_rqsts_rfo_miss/ ./rfo_test
Run Code Online (Sandbox Code Playgroud)
并得到了更合理的结果:
Tl;博士; 使用预取数字,我们看到更少的 RFO 请求rep movsb。但它似乎并没有rep movsb真正避免 RFO 请求,而只是接触较少的缓存行
| 待办事项 = | 性能事件 | 带预取 | 无预取 | 区别 |
|---|---|---|---|---|
| --------------- | --------------- | --------------- | --------------- | --------------- |
| 颞 | l2_rqsts_references | 16812993 | 4358692 | 12454301 |
| 颞 | l2_rqsts_all_rfo | 14443392 | 1981560 | 12461832 |
| 颞 | l2_rqsts_rfo_hit | 1297932 | 1038243 | 259689 |
| 颞 | l2_rqsts_rfo_miss | 13145460 | 943317 | 12202143 |
| --------------- | --------------- | --------------- | --------------- | --------------- |
| 非临时 | l2_rqsts_references | 8820287 | 1946591 | 6873696 |
| 非临时 | l2_rqsts_all_rfo | 6852605 | 346 | 6852259 |
| 非临时 | l2_rqsts_rfo_hit | 66845 | 317 | 66528 |
| 非临时 | l2_rqsts_rfo_miss | 6785760 | 29 | 6785731 |
| --------------- | --------------- | --------------- | --------------- | --------------- |
| REP_MOVSB | l2_rqsts_references | 11856549 | 7400277 | 4456272 |
| REP_MOVSB | l2_rqsts_all_rfo | 8633330 | 4194510 | 4438820 |
| REP_MOVSB | l2_rqsts_rfo_hit | 1394372 | 546 | 1393826 |
| REP_MOVSB | l2_rqsts_rfo_miss | 7238958 | 4193964 | 3044994 |
| --------------- | --------------- | --------------- | --------------- | --------------- |
| LOAD_ONLY_TEMPORAL | l2_rqsts_references | 6058269 | 619924 | 5438345 |
| LOAD_ONLY_TEMPORAL | l2_rqsts_all_rfo | 5103905 | 337 | 5103568 |
| LOAD_ONLY_TEMPORAL | l2_rqsts_rfo_hit | 438518 | 311 | 438207 |
| LOAD_ONLY_TEMPORAL | l2_rqsts_rfo_miss | 4665387 | 26 | 4665361 |
| --------------- | --------------- | --------------- | --------------- | --------------- |
| STORE_ONLY_TEMPORAL | l2_rqsts_references | 8069068 | 837616 | 7231452 |
| STORE_ONLY_TEMPORAL | l2_rqsts_all_rfo | 8033854 | 802969 | 7230885 |
| STORE_ONLY_TEMPORAL | l2_rqsts_rfo_hit | 585938 | 576955 | 8983 |
| STORE_ONLY_TEMPORAL | l2_rqsts_rfo_miss | 7447916 | 226014 | 7221902 |
| --------------- | --------------- | --------------- | --------------- | --------------- |
| STORE_ONLY_REP_STOSB | l2_rqsts_references | 4296169 | 4228643 | 67526 |
| STORE_ONLY_REP_STOSB | l2_rqsts_all_rfo | 4261756 | 4194548 | 67208 |
| STORE_ONLY_REP_STOSB | l2_rqsts_rfo_hit | 17337 | 309 | 17028 |
| STORE_ONLY_REP_STOSB | l2_rqsts_rfo_miss | 4244419 | 4194239 | 50180 |
| --------------- | --------------- | --------------- | --------------- | --------------- |
| STORE_ONLY_NON_TEMPORAL | l2_rqsts_references | 99713 | 36112 | 63601 |
| STORE_ONLY_NON_TEMPORAL | l2_rqsts_all_rfo | 64148 | 427 | 63721 |
| STORE_ONLY_NON_TEMPORAL | l2_rqsts_rfo_hit | 17091 | 398 | 16693 |
| STORE_ONLY_NON_TEMPORAL | l2_rqsts_rfo_miss | 47057 | 29 | 47028 |
| --------------- | --------------- | --------------- | --------------- | --------------- |
| 以上都不是 | l2_rqsts_references | 74074 | 27656 | 46418 |
| 以上都不是 | l2_rqsts_all_rfo | 46833 | 375 | 46458 |
| 以上都不是 | l2_rqsts_rfo_hit | 16366 | 344 | 16022 |
| 以上都不是 | l2_rqsts_rfo_miss | 30467 | 31 | 30436 |
似乎大多数 RFO 差异归结为为 memcpy预取增强型 REP MOVSB
立即准确地发出预取请求。硬件预取在检测类似 memcpy 的模式方面做得很好,但它仍然需要几次读取才能启动,并且会“过度预取”超出复制区域末尾的许多缓存行。rep movsb 确切地知道区域大小并且可以准确地预取。
这一切似乎都归结为rep movsb不预取存储地址导致需要 RFO 请求的行减少。通过STORE_ONLY_REP_STOSB 我们可以更好地了解 RFO 请求的保存位置rep movsb(假设这两个实现方式相似)。在不计算预取事件的情况下,我们看到rep movsbRFO 请求的数量与rep stosb(以及 HITS / MISSES 的细分相同)大致相同。它有大约 250 万个额外的 L2 引用,可以公平地归因于负载。
这些STORE_ONLY_REP_STOSB数字特别有趣的是,它们几乎不会随着预取数据与非预取数据而变化。这让我觉得rep stosb至少不是预取商店地址。这也对应一个事实,即我们看到的几乎没有RFO_HITS,几乎完全RFO_MISSES。另一方面,临时存储 memcpy 是预取存储地址,因此原始数字出现偏差,因为它们没有vmovdqa计算来自rep movsb.
另一个有趣的指针是STORE_ONLY_REP_STOSB与STORE_ONLY_NON_TEMORAL. 这让我认为rep movsb/rep stosb只是在存储上保存 RFO 请求,因为它没有进行额外的预取,而是使用通过缓存的临时存储。我很难调和的一件事是,似乎来自rep movsb/rep stosb两者都没有预取的存储不使用包含 RFO 的非临时存储,因此我不确定它的性能如何。
我认为rep movsb是预取负载,它在标准vmovdqa循环中做得更好。如果您查看rep movsbw/ 和 w/o 预取和差异之间的差异,LOAD_ONLY_TEMPORAL您会看到大致相同的模式,参考数字LOAD_ONLY_TEMPORAL高出约 20%,但点击数低。这将表明vmovdqa循环正在执行超过尾部的额外预取并且预取效率较低。因此rep movsb,预取加载地址的工作做得更好(因此总引用更少,命中率更高)。
以下是我从数据中想到的:
rep movsb 不会优化给定加载/存储的 RFO 请求
rep movsb不预取存储并且不使用非临时存储。因此,它对存储使用较少的 RFO 请求,因为它不会通过预取引入不必要的行。
rep movsb 预取加载并且比正常的时间加载循环做得更好。编辑4:
使用新的性能配方来衡量非核心读/写:
perf stat -a -e "uncore_imc/event=0x01,name=data_reads/" -e "uncore_imc/event=0x02,name=data_writes/" ./rfo_test
Run Code Online (Sandbox Code Playgroud)
这个想法是如果rep stosb发送 RFO-ND 那么它应该具有与movntdq. 情况似乎如此。
24,251,861 data_reads
52,130,870 data_writes
Run Code Online (Sandbox Code Playgroud)
vmovdqa ymm, (%reg). 这不是 64 字节的存储,因此应该需要带有数据的 RFO。我确实对此进行了测试,vmodqa32 zmm, (%reg)并看到了大致相同的数字。这意味着 1)zmm商店没有经过优化以跳过 RFO 以支持 ItoM,或者 2) 这些事件并不表明我认为它们是什么小心。 39,785,140 data_reads
35,225,418 data_writes
Run Code Online (Sandbox Code Playgroud)
22,680,373 data_reads
51,057,807 data_writes
Run Code Online (Sandbox Code Playgroud)
奇怪的一件事是,虽然两者的读取量都较低,STORE_ONLY_NON_TEMPORAL而STORE_ONLY_REP_STOSB写入量却较高。
有真名RFO-ND;ItoM。
它与 RFO 聚合在一起OFFCORE_REQUESTS.DEMAND_RFO。英特尔有一个性能工具,它似乎从 MSR 中汲取了它的价值,但他们不支持 ICL,到目前为止,找不到 ICL 的文档。需要更多地研究如何隔离它。
Edit5:STORE_ONLY_TEMPORAL较早写入较少的原因是零存储消除。
我的测量方法的这个问题之一是该选项不uncore_imc支持事件all-user。我稍微改变了 perf 配方以尝试缓解这种情况:
perf stat -D 1000 -C 0 -e "uncore_imc/event=0x01,name=data_reads/" -e "uncore_imc/event=0x02,name=data_wri
| 归档时间: |
|
| 查看次数: |
225 次 |
| 最近记录: |