一次性测试内存复制速度的基准测试

4

威士忌湖 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 $0, %%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

更新: 使用taskset -c 1 ./bin运行基准测试后得到了结果。

因此,在内存复制例程实现中,我在核心周期方面获得了近乎两倍的差异。 我将其解释为在常规存储器写入WB内存的情况下,我们有RFO请求竞争总线带宽,如IOM / 3.6.12中所指定的那样(强调属于我):

尽管由于非暂态存储器的64字节总线完整写入的数据带宽是WB内存的两倍,但传输8字节块会浪费总线请求带宽并提供显着较低的数据带宽。

问题1:如何进行单次拍摄的基准分析?由于启动性能和预热迭代开销,perf计数器似乎不太有用。

问题2:这样的基准测试是否正确?我在开始时考虑了cpuid,以便使用干净的CPU资源开始测量,以避免由于先前指令在执行中而造成停顿。 我添加了内存清除作为编译屏障,并添加了lfence以避免rdpmc乱序执行。


1
为什么在“warmup”中有这么多对copy_fn的调用?该函数使您能够消除写入“dest”时将发生的页面故障的开销,但我不确定为什么需要多次调用copy_fn。另外,lfence不能确保所有先前的存储已成为全局可观察到的。您可以使用mfence代替。尽管在您的处理器上,稍后从WC内存加载可能会通过mfence(与lfence相反)。此外,我认为您需要在第一个__rdpmc之后加上编译器屏障。 - Hadi Brais
@HadiBrais 另外,lfence 不能确保所有先前的存储都已经成为全局可观察状态。 同意。我插入了 lfence ,以便在执行 rdpmc 后进行序列化,但是 软件必须在 RDPMC 指令之前和/或之后插入一个序列化指令(例如 CPUID 指令) 因此 lfence 看起来不足够。此外,我认为您需要在第一个 __rdpmc 之后立即放置编译器屏障。 好主意。为什么在 warmup 中有这么多 copy_fn 的调用? 我认为这是为了预热操作码高速缓存而需要的。但是调用量是随机选择的。 - St.Antario
2
哦,没错。你可以使用序列 mfence;lfence。其实没有必要使用完全序列化的指令,但是你当然可以使用 cpuid。无论如何,这个细节只有在你想进行非常精细的周期计算时才很重要。 - Hadi Brais
1个回答

8

在可能的情况下,基准测试应以尽可能多的方式报告结果,以便进行尽可能多的"合理性检查"。为此,一些启用这些检查的方法包括:

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

其他重要的“最佳实践”原则:

任何使用RDPMC指令的测试都必须绑定到单个逻辑处理器。结果应以确认读者采用了此类绑定的方式呈现。在Linux中,强制执行这种绑定的常见方法包括使用“taskset”或“numactl --physcpubind = [n]”命令,或包括一个内联调用“sched_setaffinity()”,其中只允许一个逻辑处理器,或设置导致运行时库(例如OpenMP)将线程绑定到单个逻辑处理器的环境变量。
使用硬件性能计数器时,需要额外注意确保计数器的所有配置数据都可用并且正确描述。上面的代码使用RDPMC读取IA32_PERF_FIXED_CTR1,该计数器事件名称为CPU_CLK_UNHALTED。事件名称的修饰符取决于IA32_FIXED_CTR_CTRL(MSR 0x38d)位7:4的编程。没有通常接受的方式可以从所有可能的控制位映射到事件名称修饰符,因此最好同时提供IA32_FIXED_CTR_CTRL的完整内容和结果。
对于直接与处理器核心频率成比例的部分(例如仅涉及L1和L2缓存的指令执行和数据传输),CPU_CLK_UNHALTED性能计数器事件是正确的用于基准测试的计数器事件。内存带宽涉及处理器的部分性能与处理器频率不直接成比例。特别是,如果不强制使用固定频率操作,则使用CPU_CLK_UNHALTED将无法计算经过的时间(需要(1)和(3)中提到的)。在您的情况下,使用RDTSCP会比RDPMC更容易-- RDTSC不需要进程绑定到单个逻辑处理器,不受其他配置MSR的影响,并且允许以秒为单位直接计算经过的时间。
高级:对于涉及内存层次结构中数据传输的测试,有助于控制缓存内容和缓存内容的状态(干净或脏),并在结果中提供“之前”和“之后”状态的明确描述。鉴于数组的大小,您的代码应完全使用源和目标数组的某些组合填充所有级别的缓存,然后刷新所有这些地址,留下一个(几乎)完全充满无效(干净)条目的缓存层次结构。
高级:在基准测试中使用CPUID作为序列化指令几乎永远没有用处。尽管它保证排序,但执行时间很长- Agner Fog的“Instruction Tables”报告它在100-250个周期(可能取决于输入参数)。 (更新:在短时间间隔内进行测量始终非常棘手。CPUID指令具有长且可变的执行时间,不清楚微码实现对处理器内部状态的影响。在特定情况下可能有用,但不应将其自动包含在基准测试中。对于长时间间隔的测量,测量边界之间的乱序处理可以忽略,因此不需要CPUID。)
高级:在基准测试中使用LFENCE只有在测量非常细粒度(小于几百个周期)时才相关。有关此主题的更多说明,请参见http://sites.utexas.edu/jdm4372/2018/07/23/comments-on-timing-short-code-sections-on-intel-processors/
假设您的处理器在测试期间以其最大Turbo频率4.6 GHz运行,则报告的周期计数分别对应于9.67毫秒和5.23毫秒。将这些插入“合理性检查”中,结果如下:
- 假设第一种情况执行了一个读取、一个分配和一个写回(每个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% 峰值。
这些“合理性检查”的失败告诉我们,频率无法高达4.6 GHz(可能不高于3.0 GHz),但主要是指需要明确测量经过的时间....
您在优化手册中引用的关于流式存储效率低下的语句仅适用于不能合并为完整高速缓存线路传输的情况。您的代码按照“最佳实践”推荐方式存储输出高速缓存行的每个元素(所有写入相同行的存储指令都按顺序执行,并且每个循环只生成一个存储流)。无法完全防止硬件分解流式存储,但在您的情况下,这应该是极其罕见的-或许在百万次操作中可能会出现几次。检测部分流式存储是一个非常高级的主题,需要使用“不核心”中的文档稀少的性能计数器和/或通过查找DRAM CAS计数的增加(可能是由于其他原因)来间接检测部分流式存储。有关流式存储的更多说明请参见此处

1
在基准测试中,使用CPUID作为序列化指令几乎没有用处。这并不是很清楚为什么。在测量之前调用它以序列化所有先前的uops执行并释放所有CPU资源(如SB、LB、RS、ROB)难道不是有用的吗?这样可以使基准测试以所有资源被释放的状态开始... - St.Antario
1
听起来非常可信;我的i7-6700k台式机在完全受限于内存时喜欢将自己的时钟降至2.7GHz,当能量性能偏好设置为平衡功率或平衡性能时。(不是在完全的“性能”设置下)。这可能会通过增加非核心延迟来损害单核带宽。 :/而且它的时钟频率可能不高于3.0 GHz。 - Peter Cordes
1
3.0 GHz的频率猜测来自于假设最大DRAM带宽不超过峰值的85%,然后计算给定数据流量的最大经过时间,接着计算相应的频率以匹配报告的周期计数。3.0 GHz意味着87.2%的峰值DRAM BW,这可能(但不一定)太高了。 - John D McCalpin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接