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

St.*_*rio 4 c performance benchmarking assembly x86-64

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);
Run Code Online (Sandbox Code Playgroud)

定义:

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
Run Code Online (Sandbox Code Playgroud)

基准代码:

#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);
    }
}
Run Code Online (Sandbox Code Playgroud)

结果

avx_memcpy_forward_llss中位数:44479368 个核心周期

UPD:时间

real    0m0,217s
user    0m0,093s
sys     0m0,124s
Run Code Online (Sandbox Code Playgroud)

avx_nt_memcpy_forward_llss中位数:24053086 个核心周期

UPD:时间

real    0m0,184s
user    0m0,056s
sys     0m0,128s
Run Code Online (Sandbox Code Playgroud)

UPD:结果是在运行基准测试时得到的 taskset -c 1 ./bin

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

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

QUESTION 1:单发情况下如何做基准分析?由于性能启动开销和预热迭代开销,性能计数器似乎没有用。

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

Joh*_*pin 8

只要有可能,基准测试应该以允许尽可能多的“健全性检查”的方式报告结果。在这种情况下,启用此类检查的几种方法包括:

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

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

  1. 任何使用 RDPMC 指令的测试都必须绑定到单个逻辑处理器。结果应以向读者确认使用了这种绑定的方式呈现。在 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. 高级:对于涉及内存层次结构中数据传输的测试,有助于控制缓存内容和缓存内容的状态(干净或脏),并提供“之前”和“之后”状态的明确描述结果。给定数组的大小,您的代码应该用源数组和目标数组的某些部分组合完全填充缓存的所有级别,然后刷新所有这些地址,留下(几乎)完全充满无效的缓存层次结构(干净)条目。
  5. 高级:使用 CPUID 作为序列化指令在基准测试中几乎没有用处。虽然它保证了排序,但它也需要很长时间来执行——Agner Fog 的“指令表”报告它在 100-250 个周期(大概取决于输入参数)。 (更新:短时间间隔的测量总是非常棘手。CPUID 指令的执行时间长且可变,并且不清楚微码实现对处理器的内部状态有什么影响。在特定情况下可能会有所帮助,但不应将其视为自动包含在基准测试中的东西。对于长时间间隔的测量,跨测量边界的乱序处理可以忽略不计,因此不需要 CPUID。)
  6. 高级:在基准测试中使用 LFENCE 仅在您以非常细的粒度进行测量时才有意义——少于几百个周期。有关此主题的更多说明,请访问http://sites.utexas.edu/jdm4372/2018/07/23/comments-on-timing-short-code-sections-on-intel-processors/

如果我假设您的处理器在测试期间以 4.6 GHz 的最大 Turbo 频率运行,那么报告的周期计数分别对应于 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 计数(这可能是由于其他原因)间接检测部分流存储中使用记录不佳的性能计数器。http://sites.utexas.edu/jdm4372/2018/01/01/notes-on-non-temporal-aka-streaming-stores/