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

问题描述 投票:2回答:1

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 $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核心周期

UPD:时间

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

avx_nt_memcpy_forward_llss中位数:24053086核心周期

UPD:时间

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

UPD:使用taskset -c 1 ./bin]运行基准测试时得到的结果>

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

尽管完整的64字节总线写入的数据带宽是由于

非临时存储是总线写入WB存储器的两倍,传输8字节的数据块浪费了总线请求带宽并交付了大大降低了数据带宽。

问题1:

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

问题2:这样的基准是否正确?我一开始就考虑cpuid,以便开始使用干净的CPU资源进行测量,以避免由于先前的飞行指令而导致停顿。我添加了内存碎片作为编译屏障,并添加了lfence以免rdpmc被执行。

Whiskey Lake i7-8565U我正在尝试学习如何手动编写基准测试(不使用任何基准测试框架),该示例具有常规和...的内存复制例程示例。] >> [[< [

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

对于涉及主内存带宽的测试,结果应以允许与系统的已知峰值DRAM带宽直接比较的单位表示。对于Core i7-8565U的典型配置,这是2个通道* 8字节/传输* 24亿个传输/秒= 38.4 GB / s(另请参阅以下第(6)项。)

    对于涉及在存储器层次结构中任何地方进行数据传输的测试,结果应包括对“内存占用量”大小(访问的不同缓存行地址数量乘以缓存行大小)的清晰描述,以及重复次数的转移。您的代码在这里很容易阅读,并且大小对于主内存测试是完全合理的。
  • 对于任何定时测试,应包括绝对时间,以便与合理的定时开销进行比较。您仅使用CORE_CYCLES_UNHALTED计数器就无法直接计算经过的时间(尽管测试时间足够长,因此计时开销可以忽略不计)。
  • [其他重要的“最佳实践”原则:
  • 使用RDPMC指令的任何测试都必须绑定到单个逻辑处理器。结果的显示方式应向读者确认已采用这种绑定。在Linux中强制执行此类绑定的常用方法包括使用“任务集”或“ numactl --physcpubind = [n]”命令,或使用单个允许的逻辑处理器对“ sched_setaffinity()”进行内联调用,或设置环境变量导致运行时库(例如OpenMP)将线程绑定到单个逻辑处理器。

    1. 使用硬件性能计数器时,需要格外小心,以确保计数器的所有配置数据均可用并正确描述。上面的代码使用RDPMC读取IA32_PERF_FIXED_CTR1,其事件名称为CPU_CLK_UNHALTED。事件名称的修饰符取决于IA32_FIXED_CTR_CTRL(MSR 0x38d)位7:4的编程。从所有可能的控制位到事件名称修饰符的映射没有普遍接受的方式,因此最好提供IA32_FIXED_CTR_CTRL的完整内容以及结果。
    2. CPU_CLK_UNHALTED性能计数器事件是用于对处理器的部分行为进行基准测试的正确方法,这些行为的行为与处理器核心频率直接成比例,例如指令执行和仅涉及L1和L2高速缓存的数据传输。内存带宽涉及处理器的某些部分,这些部分的性能不会直接随处理器频率扩展。特别是,在不强制执行固定频率操作的情况下使用CPU_CLK_UNHALTED使得无法计算经过时间(上述(1)和(3)要求)。在您的情况下,RDTSP比RDPMC更容易-RDTSC不需要将进程绑定到单个逻辑处理器,它不受其他配置MSR的影响,并且可以直接计算经过的时间(以秒为单位)。
    3. [高级:对于涉及在存储器层次结构中进行数据传输的测试,有助于控制缓存内容和缓存内容的状态(干净或脏),并提供对“之前”和“之后”的明确描述是有帮助的状态以及结果。给定数组的大小,您的代码应使用源数组和目标数组的部分组成部分完全填充缓存的所有级别,然后刷新所有这些地址,从而使(几乎)完全无效的缓存层次结构(干净)条目。
  • 高级:使用CPUID作为序列化指令几乎对基准测试没有用。尽管它保证了排序,但执行时间也很长-Agner Fog的“指令表”以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计数(可能是由于其他原因)间接检测部分流存储。有关流媒体商店的更多说明,请访问http://sites.utexas.edu/jdm4372/2018/01/01/notes-on-non-temporal-aka-streaming-stores/
    c performance assembly x86-64 benchmarking
    1个回答
    3
    投票

    对于涉及主内存带宽的测试,结果应以允许与系统的已知峰值DRAM带宽直接比较的单位表示。对于Core i7-8565U的典型配置,这是2个通道* 8字节/传输* 24亿个传输/秒= 38.4 GB / s(另请参阅以下第(6)项。)

      对于涉及在存储器层次结构中任何地方进行数据传输的测试,结果应包括对“内存占用量”大小(访问的不同缓存行地址数量乘以缓存行大小)的清晰描述,以及重复次数的转移。您的代码在这里很容易阅读,并且大小对于主内存测试是完全合理的。
    © www.soinside.com 2019 - 2024. All rights reserved.