一次性对内存副本进行基准测试

Benchmarking memory copy in a single shot

Whiskey Lake i7-8565U

我正在尝试学习如何通过手动(不使用任何基准测试框架)在内存复制例程示例中手动编写基准测试,并定期和非临时写入 WB 内存,并想询问一些有点评论。


声明:

void *avx_memcpy_forward_llss(void *restrict, const void *restrict, size_t);

void *avx_nt_memcpy_forward_llss(void *restrict, const void *restrict, size_t);

定义:

avx_memcpy_forward_llss:
    shr rdx, 0x3
    xor rcx, rcx
avx_memcpy_forward_loop_llss:
    vmovdqa ymm0, [rsi + 8*rcx]
    vmovdqa ymm1, [rsi + 8*rcx + 0x20]
    vmovdqa [rdi + rcx*8], ymm0
    vmovdqa [rdi + rcx*8 + 0x20], ymm1
    add rcx, 0x08
    cmp rdx, rcx
    ja avx_memcpy_forward_loop_llss
    ret

avx_nt_memcpy_forward_llss:
    shr rdx, 0x3
    xor rcx, rcx
avx_nt_memcpy_forward_loop_llss:
    vmovdqa ymm0, [rsi + 8*rcx]
    vmovdqa ymm1, [rsi + 8*rcx + 0x20]
    vmovntdq [rdi + rcx*8], ymm0
    vmovntdq [rdi + rcx*8 + 0x20], ymm1
    add rcx, 0x08
    cmp rdx, rcx
    ja avx_nt_memcpy_forward_loop_llss
    ret

基准代码:

#include <stdio.h>
#include <inttypes.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <immintrin.h>
#include <x86intrin.h>
#include "memcopy.h"

#define BUF_SIZE 128 * 1024 * 1024

_Alignas(64) char src[BUF_SIZE];
_Alignas(64) char dest[BUF_SIZE];

static inline void warmup(unsigned wa_iterations, void *(*copy_fn)(void *, const void *, size_t));
static inline void cache_flush(char *buf, size_t size);
static inline void generate_data(char *buf, size_t size);

uint64_t run_benchmark(unsigned wa_iteration, void *(*copy_fn)(void *, const void *, size_t)){
    generate_data(src, sizeof src);
    warmup(4, copy_fn); 
    cache_flush(src, sizeof src);
    cache_flush(dest, sizeof dest);
    __asm__ __volatile__("mov [=12=], %%rax\n cpuid":::"rax", "rbx", "rcx", "rdx", "memory"); 
    uint64_t cycles_start = __rdpmc((1 << 30) + 1); 
    copy_fn(dest, src, sizeof src); 
    __asm__ __volatile__("lfence" ::: "memory"); 
    uint64_t cycles_end = __rdpmc((1 << 30) + 1); 
    return cycles_end - cycles_start; 
}

int main(void){
    uint64_t single_shot_result = run_benchmark(1024, avx_memcpy_forward_llss);
    printf("Core clock cycles = %" PRIu64 "\n", single_shot_result);
}

static inline void warmup(unsigned wa_iterations, void *(*copy_fn)(void *, const void *, size_t)){
    while(wa_iterations --> 0){
        copy_fn(dest, src, sizeof src);
        copy_fn(dest, src, sizeof src);
        copy_fn(dest, src, sizeof src);
        copy_fn(dest, src, sizeof src);
        copy_fn(dest, src, sizeof src);
        copy_fn(dest, src, sizeof src);
        copy_fn(dest, src, sizeof src);
        copy_fn(dest, src, sizeof src);
    }
}

static inline void generate_data(char *buf, size_t sz){
    int fd = open("/dev/urandom", O_RDONLY);
    read(fd, buf, sz);
}

static inline void cache_flush(char *buf, size_t sz){
    for(size_t i = 0; i < sz; i+=_SC_LEVEL1_DCACHE_LINESIZE){
        _mm_clflush(buf + i);
    }
}

结果:

avx_memcpy_forward_llss中位数:44479368个核心周期

更新:时间

real    0m0,217s
user    0m0,093s
sys     0m0,124s

avx_nt_memcpy_forward_llss 中位数:24053086 个核心周期

更新:时间

real    0m0,184s
user    0m0,056s
sys     0m0,128s

UPD:当运行基准与taskset -c 1 ./bin

时得到结果

因此,内存复制例程实现之间的核心周期差异几乎是 2 倍。我将其解释为在常规存储到 WB 内存的情况下,我们有 RFO 请求在总线带宽上竞争,因为它在 IOM/3.6.12(强调我的):

中指定

Although the data bandwidth of full 64-byte bus writes due to non-temporal stores is twice that of bus writes to WB memory, transferring 8-byte chunks wastes bus request bandwidth and delivers significantly lower data bandwidth.

问题1:单发情况下如何做benchmark分析?由于 perf 启动开销和预热迭代开销,Perf 计数器似乎没有用。

问题 2: 这样的基准是否正确。我在开始时考虑了 cpuid,以便开始使用干净的 CPU 资源进行测量,以避免由于先前的飞行指令而导致失速。我添加了 memory clobbers 作为编译屏障和 lfence 以避免 rdpmc 被执行 OoO.

只要有可能,基准测试报告结果的方式应尽可能允许 "sanity-checking"。在这种情况下,启用此类检查的几种方法包括:

  1. 对于涉及主内存带宽的测试,结果应以允许与系统的已知峰值 DRAM 带宽直接比较的单位显示。对于 Core i7-8565U 的典型配置,这是 2 通道 * 8 Bytes/transfer * 24 亿 transfers/sec = 38.4 GB/s(另请参见下面的第 (6) 项。)
  2. 对于涉及在内存层次结构中任何位置传输数据的测试,结果应包括对 "memory footprint" 大小的清晰描述(访问的不同缓存行地址数乘以缓存行大小)和传输的重复次数。您的代码在这里很容易阅读,大小对于主内存测试来说是完全合理的。
  3. 对于任何计时测试,都应包括绝对时间,以便与合理的计时开销进行比较。您仅使用 CORE_CYCLES_UNHALTED 计数器就无法直接计算经过的时间(尽管测试显然足够长,时间开销可以忽略不计)。

其他重要的"best practice"原则:

  1. 任何使用 RDPMC 指令的测试都必须绑定到单个逻辑处理器。结果的呈现方式应向 reader 确认已采用此类绑定。在 Linux 中强制执行此类绑定的常见方法包括使用 "taskset" 或 "numactl --physcpubind=[n]" 命令,或使用单个允许的逻辑处理器包含对 "sched_setaffinity()" 的内联调用,或设置环境导致运行时库(例如 OpenMP)将线程绑定到单个逻辑处理器的变量。
  2. 使用硬件性能计数器时,需要格外小心,以确保计数器的所有配置数据都可用且描述正确。上面的代码使用 RDPMC 读取 IA32_PERF_FIXED_CTR1,它有一个事件名称 CPU_CLK_UNHALTED。事件名称的修饰符取决于 IA32_FIXED_CTR_CTRL (MSR 0x38d) 位 7:4 的编程。从所有可能的控制位到事件名称修饰符的映射没有普遍接受的方法,因此最好提供 IA32_FIXED_CTR_CTRL 的完整内容以及结果。
  3. CPU_CLK_UNHALTED 性能计数器事件是用于处理器部分基准测试的正确事件,其行为直接与处理器核心频率成比例——例如仅涉及 L1 和 L2 的指令执行和数据传输缓存。内存带宽涉及处理器的部分,其性能 直接与处理器频率成比例。特别是,如果使用 CPU_CLK_UNHALTED 而没有强制固定频率操作,则无法计算经过的时间(上述 (1) 和 (3) 所要求的)。在您的情况下,RDTSCP 比 RDPMC 更容易——RDTSC 不需要将进程绑定到单个逻辑处理器,它不受其他配置 MSR 的影响,并且它允许以秒为单位直接计算经过的时间。
  4. Advanced:对于涉及内存层次结构中数据传输的测试,有助于控制缓存内容和缓存内容的状态(干净或脏),并提供对"before" 和 "after" 状态以及结果。给定数组的大小,您的代码应该用源数组和目标数组的某些部分的某种组合完全填充缓存的所有级别,然后刷新所有这些地址,留下一个(几乎)完全充满无效的缓存层次结构(干净的)条目。
  5. 高级:使用 CPUID 作为序列化指令在基准测试中几乎没有用。虽然它保证了顺序,但它也需要很长时间才能执行——Agner Fog 的 "Instruction Tables" 报告它在 100-250 个周期(大概取决于输入参数)。 (更新:短间隔的测量总是非常棘手。CPUID 指令的执行时间长且可变,目前尚不清楚微码实现对处理器内部状态有何影响。可能是在特定情况下有帮助,但不应将其视为自动包含在基准测试中的东西。对于长间隔的测量,跨测量边界的乱序处理可以忽略不计,因此不需要 CPUID。)
  6. 高级:仅当您以非常精细的粒度(少于几百个周期)进行测量时,在基准测试中使用 LFENCE 才有意义。 http://sites.utexas.edu/jdm4372/2018/07/23/comments-on-timing-short-code-sections-on-intel-processors/
  7. 有关此主题的更多说明

如果我假设您的处理器在测试期间 运行 的最大 Turbo 频率为 4.6 GHz,则报告的周期计数分别对应于 9.67 毫秒和 5.23 毫秒。将这些插入 "sanity check" 显示:

  • 假设第一种情况执行一次读,一次分配,一次写回(每次128MiB),对应的DRAM流量为27.8GB/s + 13.9 GB/s = 41.6 GB/s == 峰值的 108%。
  • 假设第二种情况执行一次读取和一次流式存储(每个128MiB),对应的DRAM流量为25.7 GB/s + 25.7 GB/s = 51.3 GB/s = 134峰值百分比。

这些 "sanity checks" 的失败告诉我们频率不可能高达 4.6 GHz(并且可能不高于 3.0 GHz),但主要只是指出需要测量经过的时间时间明确....

优化手册中关于流式存储效率低下的引述仅适用于无法合并为完整缓存行传输的情况。您的代码按照 "best practice" 建议存储到输出缓存行的每个元素(写入同一行的所有存储指令连续执行并且每个循环仅生成一个存储流)。不可能完全阻止硬件破坏流媒体商店,但在您的情况下,这种情况应该极为罕见——也许是百万分之几。检测部分流媒体存储是一个非常高级的主题,需要在 "uncore" and/or 中使用记录不完整的性能计数器通过查找升高的 DRAM CAS 计数(这可能是由于其他原因)。有关流媒体商店的更多说明位于 http://sites.utexas.edu/jdm4372/2018/01/01/notes-on-non-temporal-aka-streaming-stores/