将带有 NT 存储的 64 字节内存复制到一个完整的缓存行与 2 个连续的部分缓存行
Copying 64 bytes of memory with NT stores to one full cache line vs. 2 consecutive partial cache lines
我正在阅读有关写入组合内存的英特尔优化手册,并编写了基准测试以了解其工作原理。这些是我 运行 基准测试的 2 个函数:
memcopy.h
:
void avx_ntcopy_cache_line(void *dest, const void *src);
void avx_ntcopy_64_two_cache_lines(void *dest, const void *src);
memcopy.S
:
avx_ntcopy_cache_line:
vmovdqa ymm0, [rdi]
vmovdqa ymm1, [rdi + 0x20]
vmovntdq [rsi], ymm0
vmovntdq [rsi + 0x20], ymm1
;intentionally no sfence after nt-store
ret
avx_ntcopy_64_two_cache_lines:
vmovdqa ymm0, [rdi]
vmovdqa ymm1, [rdi + 0x40]
vmovntdq [rsi], ymm0
vmovntdq [rsi + 0x40], ymm1
;intentionally no sfence after nt-store
ret
基准测试的主要功能如下所示:
#include <stdlib.h>
#include <inttypes.h>
#include <x86intrin.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include "memcopy.h"
#define ITERATIONS 1000000
//As @HadiBrais noted, there might be an issue with 4K aliasing
_Alignas(64) char src[128];
_Alignas(64) char dest[128];
static void run_benchmark(unsigned runs, unsigned run_iterations,
void (*fn)(void *, const void*), void *dest, const void* src);
int main(void){
int fd = open("/dev/urandom", O_RDONLY);
read(fd, src, sizeof src);
run_benchmark(20, ITERATIONS, avx_ntcopy_cache_line, dest, src);
run_benchmark(20, ITERATIONS, avx_ntcopy_64_two_cache_lines, dest, src);
}
static int uint64_compare(const void *u1, const void *u2){
uint64_t uint1 = *(uint64_t *) u1;
uint64_t uint2 = *(uint64_t *) u2;
if(uint1 < uint2){
return -1;
} else if (uint1 == uint2){
return 0;
} else {
return 1;
}
}
static inline uint64_t benchmark_2cache_lines_copy_function(unsigned iterations, void (*fn)(void *, const void *),
void *restrict dest, const void *restrict src){
uint64_t *results = malloc(iterations * sizeof(uint64_t));
unsigned idx = iterations;
while(idx --> 0){
uint64_t start = __rdpmc((1<<30)+1);
fn(dest, src);
fn(dest, src);
fn(dest, src);
fn(dest, src);
fn(dest, src);
fn(dest, src);
fn(dest, src);
fn(dest, src);
fn(dest, src);
fn(dest, src);
fn(dest, src);
fn(dest, src);
fn(dest, src);
fn(dest, src);
fn(dest, src);
fn(dest, src);
uint64_t finish = __rdpmc((1<<30)+1);
results[idx] = (finish - start) >> 4;
}
qsort(results, iterations, sizeof *results, uint64_compare);
//median
return results[iterations >> 1];
}
static void run_benchmark(unsigned runs, unsigned run_iterations,
void (*fn)(void *, const void*), void *dest, const void* src){
unsigned current_run = 1;
while(current_run <= runs){
uint64_t time = benchmark_2cache_lines_copy_function(run_iterations, fn, dest, src);
printf("Run %d result: %lu\n", current_run, time);
current_run++;
}
}
使用选项编译
-Werror \
-Wextra
-Wall \
-pedantic \
-Wno-stack-protector \
-g3 \
-O3 \
-Wno-unused-result \
-Wno-unused-parameter
和运行我得到的基准测试结果如下:
我. avx_ntcopy_cache_line
:
Run 1 result: 61
Run 2 result: 61
Run 3 result: 61
Run 4 result: 61
Run 5 result: 61
Run 6 result: 61
Run 7 result: 61
Run 8 result: 61
Run 9 result: 61
Run 10 result: 61
Run 11 result: 61
Run 12 result: 61
Run 13 result: 61
Run 14 result: 61
Run 15 result: 61
Run 16 result: 61
Run 17 result: 61
Run 18 result: 61
Run 19 result: 61
Run 20 result: 61
perf
:
Performance counter stats for './bin':
3 503 775 289 L1-dcache-loads (18,87%)
91 965 805 L1-dcache-load-misses # 2,62% of all L1-dcache hits (18,94%)
2 041 496 256 L1-dcache-stores (19,01%)
5 461 440 LLC-loads (19,08%)
1 108 179 LLC-load-misses # 20,29% of all LL-cache hits (19,10%)
18 028 817 LLC-stores (9,55%)
116 865 915 l2_rqsts.all_pf (14,32%)
0 sw_prefetch_access.t1_t2 (19,10%)
666 096 l2_lines_out.useless_hwpf (19,10%)
47 701 696 l2_rqsts.pf_hit (19,10%)
62 556 656 l2_rqsts.pf_miss (19,10%)
4 568 231 load_hit_pre.sw_pf (19,10%)
17 113 190 l2_rqsts.rfo_hit (19,10%)
15 248 685 l2_rqsts.rfo_miss (19,10%)
54 460 370 LD_BLOCKS_PARTIAL.ADDRESS_ALIAS (19,10%)
18 469 040 693 uops_retired.stall_cycles (19,10%)
16 796 868 661 uops_executed.stall_cycles (19,10%)
18 315 632 129 uops_issued.stall_cycles (19,05%)
16 176 115 539 resource_stalls.sb (18,98%)
16 424 440 816 resource_stalls.any (18,92%)
22 692 338 882 cycles (18,85%)
5,780512545 seconds time elapsed
5,740239000 seconds user
0,040001000 seconds sys
II. avx_ntcopy_64_two_cache_lines
:
Run 1 result: 6
Run 2 result: 6
Run 3 result: 6
Run 4 result: 6
Run 5 result: 6
Run 6 result: 6
Run 7 result: 6
Run 8 result: 6
Run 9 result: 6
Run 10 result: 6
Run 11 result: 6
Run 12 result: 6
Run 13 result: 6
Run 14 result: 6
Run 15 result: 6
Run 16 result: 6
Run 17 result: 6
Run 18 result: 6
Run 19 result: 6
Run 20 result: 6
perf
:
Performance counter stats for './bin':
3 095 792 486 L1-dcache-loads (19,26%)
82 194 718 L1-dcache-load-misses # 2,66% of all L1-dcache hits (18,99%)
1 793 291 250 L1-dcache-stores (19,00%)
4 612 503 LLC-loads (19,01%)
975 438 LLC-load-misses # 21,15% of all LL-cache hits (18,94%)
15 707 916 LLC-stores (9,47%)
97 928 734 l2_rqsts.all_pf (14,20%)
0 sw_prefetch_access.t1_t2 (19,21%)
532 203 l2_lines_out.useless_hwpf (19,19%)
35 394 752 l2_rqsts.pf_hit (19,20%)
56 303 030 l2_rqsts.pf_miss (19,20%)
6 197 253 load_hit_pre.sw_pf (18,93%)
13 458 517 l2_rqsts.rfo_hit (18,94%)
14 031 767 l2_rqsts.rfo_miss (18,93%)
36 406 273 LD_BLOCKS_PARTIAL.ADDRESS_ALIAS (18,94%)
2 213 339 719 uops_retired.stall_cycles (18,93%)
1 225 185 268 uops_executed.stall_cycles (18,94%)
1 943 649 682 uops_issued.stall_cycles (18,94%)
126 401 004 resource_stalls.sb (19,20%)
202 537 285 resource_stalls.any (19,20%)
5 676 443 982 cycles (19,18%)
1,521271014 seconds time elapsed
1,483660000 seconds user
0,032253000 seconds sys
可以看出,测量结果相差10倍
我的解读:
如 Intel Optimization Manual/3.6.9
中所述:
writes to different parts of the same cache line can be grouped into a
single, full-cache-line bus transaction instead of going across the
bus (since they are not cached) as several partial writes
我假设在 avx_ntcopy_cache_line
的情况下,我们有完整的 64 字节写入启动总线事务将它们写出,这禁止 rdtsc
乱序执行。
相比之下,在 avx_ntcopy_64_two_cache_lines
的情况下,我们将 32 个字节写入不同的高速缓存行,进入 WC 缓冲区,并且未触发总线事务。这使得 rdtsc
被乱序执行。
这种解释看起来非常可疑,并且不符合 bus-cycles
区别:
avx_ntcopy_cache_line: 131 454 700
avx_ntcopy_64_two_cache_lines: 31 957 050
问题:这种测量差异的真正原因是什么?
假设:(完全)重叠存储到尚未刷新的 WC 缓冲区可以合并到其中。完成一条线会立即触发刷新,所有那些离开核心的商店都很慢。
您报告的全行版本比 2 部分行版本多 resource_stalls.sb
100 倍。这与这个解释是一致的。
如果 2_lines 可以将 NT 存储提交到现有的 WC 缓冲区 (LFB),则存储缓冲区可以跟上存储指令的执行速度,通常会在其他方面成为瓶颈。 (考虑到每对 loads/stores 的 call/ret 开销,可能只是前端。当然 call
确实包括一个商店。)您的 perf
结果显示 18 亿存储(到 L1)超过 57 亿个周期,因此完全在 1 store/cycle 限制内,我们可能期望存储进入 WC 缓冲区。
但是如果 WC 缓冲区被刷新,这发生在一行完全写入时,它必须离开核心(这很慢),占用LFB 暂时无法用于提交以后的 NT 存储。当存储不能离开存储缓冲区时,它就会被填满,并且内核会停止为新的存储指令分配资源以进入后端。 (特别是 issue/rename/allocate 阶段停顿。)
您可能可以通过任何 L2、L3、SQ、offcore req/resp 事件更清楚地看到这种效果,这些事件会接收 L1 之外的所有这些流量。您包括了一些 L2 计数器,但这些计数器可能不会接收通过 L2 的 NT 存储。
建议 NT 存储需要更长的时间让 LFB "hand off" 到内存层次结构的外层 ,在请求开始它的旅程。 (也许是为了确保一个核心总是可以重新加载它刚刚存储的内容,或者不会丢失对正在运行的 NT 存储的跟踪以保持与 MESI 的一致性。)后来的 sfence
还需要知道早期的 NT 存储何时有对其他核心可见,因此在此之前的任何时候我们都不能让它们不可见。
即使情况并非如此,所有这些 NT 存储请求在某处仍将存在吞吐量瓶颈。所以另一种可能的机制是它们填满了一些缓冲区,然后核心不能再交出 LFB,所以它用完了 LFB 来提交 NT 存储,然后 SB 填充停滞分配。
一旦它们到达内存控制器,它们可能会合并,而无需每个都需要通过实际的外部内存总线进行突发传输,但是从核心通过非核心到内存控制器的路径并不短。
即使对每 32 个存储执行 2x rdpmc
也不会减慢 CPU 足以防止存储缓冲区填满;您所看到的取决于 运行 这在一个相对紧密的循环中,而不是从一个空的存储缓冲区开始的一次性执行。此外,您关于 rdpmc
或 rdtsc
的建议不会重新排序。 WC 缓冲区刷新是零意义。商店的执行不是命令wrt。执行 rdtsc
.
TL:DR:您的 rdpmc
对单个商店组计时没有帮助,如果有任何东西通过减慢没有的快速情况来隐藏一些性能差异的话存储缓冲区存在瓶颈。
我正在阅读有关写入组合内存的英特尔优化手册,并编写了基准测试以了解其工作原理。这些是我 运行 基准测试的 2 个函数:
memcopy.h
:
void avx_ntcopy_cache_line(void *dest, const void *src);
void avx_ntcopy_64_two_cache_lines(void *dest, const void *src);
memcopy.S
:
avx_ntcopy_cache_line:
vmovdqa ymm0, [rdi]
vmovdqa ymm1, [rdi + 0x20]
vmovntdq [rsi], ymm0
vmovntdq [rsi + 0x20], ymm1
;intentionally no sfence after nt-store
ret
avx_ntcopy_64_two_cache_lines:
vmovdqa ymm0, [rdi]
vmovdqa ymm1, [rdi + 0x40]
vmovntdq [rsi], ymm0
vmovntdq [rsi + 0x40], ymm1
;intentionally no sfence after nt-store
ret
基准测试的主要功能如下所示:
#include <stdlib.h>
#include <inttypes.h>
#include <x86intrin.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include "memcopy.h"
#define ITERATIONS 1000000
//As @HadiBrais noted, there might be an issue with 4K aliasing
_Alignas(64) char src[128];
_Alignas(64) char dest[128];
static void run_benchmark(unsigned runs, unsigned run_iterations,
void (*fn)(void *, const void*), void *dest, const void* src);
int main(void){
int fd = open("/dev/urandom", O_RDONLY);
read(fd, src, sizeof src);
run_benchmark(20, ITERATIONS, avx_ntcopy_cache_line, dest, src);
run_benchmark(20, ITERATIONS, avx_ntcopy_64_two_cache_lines, dest, src);
}
static int uint64_compare(const void *u1, const void *u2){
uint64_t uint1 = *(uint64_t *) u1;
uint64_t uint2 = *(uint64_t *) u2;
if(uint1 < uint2){
return -1;
} else if (uint1 == uint2){
return 0;
} else {
return 1;
}
}
static inline uint64_t benchmark_2cache_lines_copy_function(unsigned iterations, void (*fn)(void *, const void *),
void *restrict dest, const void *restrict src){
uint64_t *results = malloc(iterations * sizeof(uint64_t));
unsigned idx = iterations;
while(idx --> 0){
uint64_t start = __rdpmc((1<<30)+1);
fn(dest, src);
fn(dest, src);
fn(dest, src);
fn(dest, src);
fn(dest, src);
fn(dest, src);
fn(dest, src);
fn(dest, src);
fn(dest, src);
fn(dest, src);
fn(dest, src);
fn(dest, src);
fn(dest, src);
fn(dest, src);
fn(dest, src);
fn(dest, src);
uint64_t finish = __rdpmc((1<<30)+1);
results[idx] = (finish - start) >> 4;
}
qsort(results, iterations, sizeof *results, uint64_compare);
//median
return results[iterations >> 1];
}
static void run_benchmark(unsigned runs, unsigned run_iterations,
void (*fn)(void *, const void*), void *dest, const void* src){
unsigned current_run = 1;
while(current_run <= runs){
uint64_t time = benchmark_2cache_lines_copy_function(run_iterations, fn, dest, src);
printf("Run %d result: %lu\n", current_run, time);
current_run++;
}
}
使用选项编译
-Werror \
-Wextra
-Wall \
-pedantic \
-Wno-stack-protector \
-g3 \
-O3 \
-Wno-unused-result \
-Wno-unused-parameter
和运行我得到的基准测试结果如下:
我. avx_ntcopy_cache_line
:
Run 1 result: 61
Run 2 result: 61
Run 3 result: 61
Run 4 result: 61
Run 5 result: 61
Run 6 result: 61
Run 7 result: 61
Run 8 result: 61
Run 9 result: 61
Run 10 result: 61
Run 11 result: 61
Run 12 result: 61
Run 13 result: 61
Run 14 result: 61
Run 15 result: 61
Run 16 result: 61
Run 17 result: 61
Run 18 result: 61
Run 19 result: 61
Run 20 result: 61
perf
:
Performance counter stats for './bin':
3 503 775 289 L1-dcache-loads (18,87%)
91 965 805 L1-dcache-load-misses # 2,62% of all L1-dcache hits (18,94%)
2 041 496 256 L1-dcache-stores (19,01%)
5 461 440 LLC-loads (19,08%)
1 108 179 LLC-load-misses # 20,29% of all LL-cache hits (19,10%)
18 028 817 LLC-stores (9,55%)
116 865 915 l2_rqsts.all_pf (14,32%)
0 sw_prefetch_access.t1_t2 (19,10%)
666 096 l2_lines_out.useless_hwpf (19,10%)
47 701 696 l2_rqsts.pf_hit (19,10%)
62 556 656 l2_rqsts.pf_miss (19,10%)
4 568 231 load_hit_pre.sw_pf (19,10%)
17 113 190 l2_rqsts.rfo_hit (19,10%)
15 248 685 l2_rqsts.rfo_miss (19,10%)
54 460 370 LD_BLOCKS_PARTIAL.ADDRESS_ALIAS (19,10%)
18 469 040 693 uops_retired.stall_cycles (19,10%)
16 796 868 661 uops_executed.stall_cycles (19,10%)
18 315 632 129 uops_issued.stall_cycles (19,05%)
16 176 115 539 resource_stalls.sb (18,98%)
16 424 440 816 resource_stalls.any (18,92%)
22 692 338 882 cycles (18,85%)
5,780512545 seconds time elapsed
5,740239000 seconds user
0,040001000 seconds sys
II. avx_ntcopy_64_two_cache_lines
:
Run 1 result: 6
Run 2 result: 6
Run 3 result: 6
Run 4 result: 6
Run 5 result: 6
Run 6 result: 6
Run 7 result: 6
Run 8 result: 6
Run 9 result: 6
Run 10 result: 6
Run 11 result: 6
Run 12 result: 6
Run 13 result: 6
Run 14 result: 6
Run 15 result: 6
Run 16 result: 6
Run 17 result: 6
Run 18 result: 6
Run 19 result: 6
Run 20 result: 6
perf
:
Performance counter stats for './bin':
3 095 792 486 L1-dcache-loads (19,26%)
82 194 718 L1-dcache-load-misses # 2,66% of all L1-dcache hits (18,99%)
1 793 291 250 L1-dcache-stores (19,00%)
4 612 503 LLC-loads (19,01%)
975 438 LLC-load-misses # 21,15% of all LL-cache hits (18,94%)
15 707 916 LLC-stores (9,47%)
97 928 734 l2_rqsts.all_pf (14,20%)
0 sw_prefetch_access.t1_t2 (19,21%)
532 203 l2_lines_out.useless_hwpf (19,19%)
35 394 752 l2_rqsts.pf_hit (19,20%)
56 303 030 l2_rqsts.pf_miss (19,20%)
6 197 253 load_hit_pre.sw_pf (18,93%)
13 458 517 l2_rqsts.rfo_hit (18,94%)
14 031 767 l2_rqsts.rfo_miss (18,93%)
36 406 273 LD_BLOCKS_PARTIAL.ADDRESS_ALIAS (18,94%)
2 213 339 719 uops_retired.stall_cycles (18,93%)
1 225 185 268 uops_executed.stall_cycles (18,94%)
1 943 649 682 uops_issued.stall_cycles (18,94%)
126 401 004 resource_stalls.sb (19,20%)
202 537 285 resource_stalls.any (19,20%)
5 676 443 982 cycles (19,18%)
1,521271014 seconds time elapsed
1,483660000 seconds user
0,032253000 seconds sys
可以看出,测量结果相差10倍
我的解读:
如 Intel Optimization Manual/3.6.9
中所述:
writes to different parts of the same cache line can be grouped into a single, full-cache-line bus transaction instead of going across the bus (since they are not cached) as several partial writes
我假设在 avx_ntcopy_cache_line
的情况下,我们有完整的 64 字节写入启动总线事务将它们写出,这禁止 rdtsc
乱序执行。
相比之下,在 avx_ntcopy_64_two_cache_lines
的情况下,我们将 32 个字节写入不同的高速缓存行,进入 WC 缓冲区,并且未触发总线事务。这使得 rdtsc
被乱序执行。
这种解释看起来非常可疑,并且不符合 bus-cycles
区别:
avx_ntcopy_cache_line: 131 454 700
avx_ntcopy_64_two_cache_lines: 31 957 050
问题:这种测量差异的真正原因是什么?
假设:(完全)重叠存储到尚未刷新的 WC 缓冲区可以合并到其中。完成一条线会立即触发刷新,所有那些离开核心的商店都很慢。
您报告的全行版本比 2 部分行版本多 resource_stalls.sb
100 倍。这与这个解释是一致的。
如果 2_lines 可以将 NT 存储提交到现有的 WC 缓冲区 (LFB),则存储缓冲区可以跟上存储指令的执行速度,通常会在其他方面成为瓶颈。 (考虑到每对 loads/stores 的 call/ret 开销,可能只是前端。当然 call
确实包括一个商店。)您的 perf
结果显示 18 亿存储(到 L1)超过 57 亿个周期,因此完全在 1 store/cycle 限制内,我们可能期望存储进入 WC 缓冲区。
但是如果 WC 缓冲区被刷新,这发生在一行完全写入时,它必须离开核心(这很慢),占用LFB 暂时无法用于提交以后的 NT 存储。当存储不能离开存储缓冲区时,它就会被填满,并且内核会停止为新的存储指令分配资源以进入后端。 (特别是 issue/rename/allocate 阶段停顿。)
您可能可以通过任何 L2、L3、SQ、offcore req/resp 事件更清楚地看到这种效果,这些事件会接收 L1 之外的所有这些流量。您包括了一些 L2 计数器,但这些计数器可能不会接收通过 L2 的 NT 存储。
sfence
还需要知道早期的 NT 存储何时有对其他核心可见,因此在此之前的任何时候我们都不能让它们不可见。
即使情况并非如此,所有这些 NT 存储请求在某处仍将存在吞吐量瓶颈。所以另一种可能的机制是它们填满了一些缓冲区,然后核心不能再交出 LFB,所以它用完了 LFB 来提交 NT 存储,然后 SB 填充停滞分配。
一旦它们到达内存控制器,它们可能会合并,而无需每个都需要通过实际的外部内存总线进行突发传输,但是从核心通过非核心到内存控制器的路径并不短。
即使对每 32 个存储执行 2x rdpmc
也不会减慢 CPU 足以防止存储缓冲区填满;您所看到的取决于 运行 这在一个相对紧密的循环中,而不是从一个空的存储缓冲区开始的一次性执行。此外,您关于 rdpmc
或 rdtsc
的建议不会重新排序。 WC 缓冲区刷新是零意义。商店的执行不是命令wrt。执行 rdtsc
.
TL:DR:您的 rdpmc
对单个商店组计时没有帮助,如果有任何东西通过减慢没有的快速情况来隐藏一些性能差异的话存储缓冲区存在瓶颈。