为什么使用非临时存储指令不能减少内存带宽使用?(写入似乎正在生成额外的读取)

Why using non-temporal store instructions cannot reduce memory bandwidth usage? (Writes seem to be generating extra reads)

提问人:Frontier_Setter 提问时间:10/20/2023 最后编辑:Frontier_Setter 更新时间:10/24/2023 访问量:106

问:

我想使用非临时指令来减少 memcpy 过程中写入分配产生的读取带宽。优化后的预期读写带宽应相同,均等于实际数据处理带宽。

但我在实验中发现,内存读取带宽仍然是写入带宽的 1.7 倍。

我的代码是使用内联汇编编写的,核心逻辑如下:

asm volatile(
        "mov    %[memarea], %%rax \n"   // rax = dst arr
        "mov    %[srcarea], %%rcx \n"   // rcx = src arr
        "1: \n" // start of write loop
        "movdqa 0*16(%%rcx), %%xmm0 \n"
        "movdqa 1*16(%%rcx), %%xmm1 \n"
        "movdqa 2*16(%%rcx), %%xmm2 \n"
        "movdqa 3*16(%%rcx), %%xmm3 \n"
        "movdqa 4*16(%%rcx), %%xmm4 \n"
        "movdqa 5*16(%%rcx), %%xmm5 \n"
        "movdqa 6*16(%%rcx), %%xmm6 \n"
        "movdqa 7*16(%%rcx), %%xmm7 \n"
        "PREFETCHNTA 8*16(%%rcx) \n"
        "PREFETCHNTA 12*16(%%rcx) \n"
        "movntdq %%xmm0, 0*16(%%rax) \n"
        "movntdq %%xmm1, 1*16(%%rax) \n"
        "movntdq %%xmm2, 2*16(%%rax) \n"
        "movntdq %%xmm3, 3*16(%%rax) \n"
        "movntdq %%xmm4, 4*16(%%rax) \n"
        "movntdq %%xmm5, 5*16(%%rax) \n"
        "movntdq %%xmm6, 6*16(%%rax) \n"
        "movntdq %%xmm7, 7*16(%%rax) \n"
        "add    $8*16, %%rax \n"
        "add    $8*16, %%rcx \n"
        // test write loop condition
        "cmp    %[end], %%rax \n"       // compare to end iterator
        "jb     1b \n"
        : 
        : [memarea] "r" (dst), [srcarea] "r" (src), [end] "r" (dst+effect_size)
        : "rax", "rcx", "xmm0", "xmm1", "xmm2", "xmm3", "xmm4", "xmm5", "xmm6", "xmm7", "cc", "memory");
    
    _mm_sfence();

在 memset 测试中,基于非临时指令的版本不会产生读取带宽,因此指令的功能确实有效。

我的使用有什么问题吗?


更新:

我用 Peter Cordes 提供的代码进行了实验

  • 处理器: Intel(R) Xeon(R) Gold 5218 CPU with 22MiB LLC

  • 操作系统: CentOS7 with kernel 5.14.0

  • 编译器:GCC 4.8.5 和 GCC 11.2.1

  • 编译选项:gcc nt_memcpy.c -o nt_memcpy.exe -O2 -msse2 -mavx -std=gnu99

  • 用于带宽监控的工具:pcm-memory

结果,数组大小为 32MB 和 1GB,在循环中调用复制函数。

  • 复制 32MB 数据时,读取速度为 4284.13 MB/s,写入速度为 3778.02 MB/s。(CPU 有 22 MiB 的 L3 缓存,所以这个测试大小太小,不能在这个 CPU 上做一个很好的测试,不像 Peter 的 i7-6700k 有 8 MiB,它几乎足够大。
  • 复制 1GB 数据时,读取速度为 6336.48 MB/s,写入速度为 3803.00 MB/s
  • 基线近乎空闲的读取速度为 100 MB/s,写入速度为 75 MB/s

此结果不符合非临时存储指令的预期行为(预期的读写带宽应接近)。

它也与彼得·科德斯(Peter Cordes)获得的结果不同 (详见注释)

我无法在配备 DDR4-2666 的 i7-6700k 上重现您的结果。读取 ~= intel_gpu_top监控的写入带宽,以从集成内存控制器获取统计信息。(读取速度约为 13400 MiB/s,写入速度约为 13900 MiB/s,而基线近乎空闲的读取速度为 1200 MiB/s,写入速度为 8 到 16 MiB/s。

将数组大小提高到 1GiB,我的 IMC 读取带宽只是空闲 + 写入带宽,因此没有多余的读取。

实验中的额外内存读取带宽可能来自哪里?


以下是 perf 收集的 pmc,未观察到重大的 rfo 事件。看来nt-store已经生效了。

# taskset -c 1 ./perf5 stat --all-user -e task-clock,context-switches,cpu-migrations,page-faults,cycles,instructions,uops_issued.any,idq.mite_uops,offcore_requests.demand_rfo,l2_rqsts.all_rfo -- ./nt_memcpy.exe

 Performance counter stats for './nt_memcpy.exe':

     88,537.13 msec task-clock                #    1.000 CPUs utilized
             0      context-switches          #    0.000 /sec
             0      cpu-migrations            #    0.000 /sec
       278,571      page-faults               #    3.146 K/sec
239,435,668,714      cycles                    #    2.704 GHz                      (93.56%)
55,340,543,407      instructions              #    0.23  insn per cycle           (94.51%)
53,026,607,051      uops_issued.any           #  598.919 M/sec                    (93.92%)
15,151,551,517      idq.mite_uops             #  171.132 M/sec                    (93.56%)
           168      offcore_requests.demand_rfo #    1.898 /sec                     (93.75%)
           994      l2_rqsts.all_rfo          #   11.227 /sec                     (92.61%)

  88.541352219 seconds time elapsed

  85.776548000 seconds user
   2.557275000 seconds sys

更新:

我使用客户端 CPU 进行了一个实验:

  • 处理器: Intel(R) Core(TM) i7-10700 CPU with 16MiB LLC
  • 操作系统: Ubunt with kernel 5.15.0
  • 编译器: gcc 9.4.0
  • 编译选项:gcc nt_memcpy.c -o nt_memcpy.exe -O2 -msse2 -mavx -std=gnu99
  • 用于带宽监控的工具:intel-gpu-top

结果,数组大小为 1GB,在循环中调用复制函数。

  • 复制 1GB 数据时,读取速度为 14260 MiB/s,写入速度为 14133 MiB/s
  • 基线近乎空闲的读取速度为 48 MB/s,写入速度为 4 MB/s

这一结果符合预期,也符合Peter获得的数据。

是服务器和客户端 CPU 的区别,还是 pcm-memoryintel-gpu-top 工具的区别?我无法在服务器上使用 intel-gpu-top(因为我没有显卡),也无法在客户端 CPU 上使用 pcm-memory(不支持架构),因此我无法相互验证。

x86 CPU 架构 Intel memcpy 内存带宽

评论

0赞 Peter Cordes 10/23/2023
您在哪个 CPU 上进行测试?您的缓冲区是否按 64 对齐,因此在执行任何可能需要 LFB 的加载之前,所有 NT 存储都到一对缓存行(因此可能会逐出 LFB,从而导致部分行写入)?此外,预取距离太短了。第一次预取与下一次加载将使用的地址相同。但这并不能解释问题所在;load-hit-prefetch 可能没问题。(除非将 NT 预取提升为正常需求负载会导致额外的离核请求......
0赞 Frontier_Setter 10/23/2023
@PeterCordes我使用的是英特尔(R)至强(R)金牌5218 CPU。源地址和目标地址均为 64 字节对齐。
0赞 Peter Cordes 10/23/2023
我无法在配备 DDR4-2666 的 i7-6700k 上重现您的结果。读取 ~= 从集成内存控制器获取统计信息的写入带宽。(读取速度约为 13400 MiB/s,写入速度约为 13900 MiB/s,而基线近乎空闲的读取速度为 1200 MiB/s,写入速度为 8 到 16 MiB/s。godbolt.org/z/4xr8qha9P 是我运行的完整程序,从 复制全局数组 ,在循环中调用它。(非零数组初始值设定项将其放入 . 32 MiB 比 L3 缓存大 4 倍,摊销页面错误。intel_gpu_topalignas(128) char srcbuf[1024*1024*32] = {1};alignas(128) char dstbuf[1024*1024*32];.data
0赞 Frontier_Setter 10/23/2023
我尝试了你的代码,得到了不同的结果。我的设备上有 22MB 的 L3 缓存空间。我复制的数据大小分别为 32MB 和 1GB,并通过 pcm-memory 获得带宽:1) 复制 32MB 数据时,我的读取速度为 4284.13 MB/s,写入速度为 3778.02 MB/s;2) 复制 1GB 数据时,我的读取速度为 6336.48 MB/s,写入速度为 3803.00 MB/s。读取速度为 100 MB/s,写入速度为 75 MB/s 的基线近乎空闲。
1赞 Frontier_Setter 10/23/2023
pcm-memory 是 Intel (github.com/intel/pcm) 提供的官方工具的一部分。根据 Intel,带宽由以下函数收集:。因此,读取和写入带宽可能分别对应于这两个事件Memory bw(GB/s) = (UNC_M_CAS_COUNT.RD +UNC_M_CAS_COUNT.WR) * 64 / 1000000000

答: 暂无答案