随机内存写入比随机内存读取慢?

Posted

技术标签:

【中文标题】随机内存写入比随机内存读取慢?【英文标题】:Random memory write is slower than random memory read? 【发布时间】:2019-11-24 03:01:51 【问题描述】:

我正在尝试计算顺序/随机内存读/写的内存访问时间。代码如下:

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <time.h>

#define PRINT_EXCECUTION_TIME(msg, code)                                       \
  do                                                                          \
    struct timeval t1, t2;                                                     \
    double elapsed;                                                            \
    gettimeofday(&t1, NULL);                                                   \
    do                                                                        \
      code;                                                                    \
     while (0);                                                               \
    gettimeofday(&t2, NULL);                                                   \
    elapsed = (t2.tv_sec - t1.tv_sec) * 1000.0;                                \
    elapsed += (t2.tv_usec - t1.tv_usec) / 1000.0;                             \
    printf(msg " time: %f ms\n", elapsed);                                     \
   while (0);

const int RUNS = 20;
const int N = (1 << 27) - 1;
int *data;

int seqR() 
  register int res = 0;
  register int *data_p = data;
  register int pos = 0;

  for (register int j = 0; j < RUNS; j++) 
    for (register int i = 0; i < N; i++) 
      pos = (pos + 1) & N;
      res = data_p[pos];
    
  

  return res;


int seqW() 
  register int res = 0;
  register int *data_p = data;
  register int pos = 0;

  for (register int j = 0; j < RUNS; j++) 
    for (register int i = 0; i < N; i++) 
      pos = (pos + 1) & N;
      data_p[pos] = res;
    
  

  return res;


int rndR() 
  register int res = 0;
  register int *data_p = data;
  register int pos = 0;

  for (register int j = 0; j < RUNS; j++) 
    for (register int i = 0; i < N; i++) 
      pos = (pos + i) & N;
      res = data_p[pos];
    
  

  return res;


int rndW() 
  register int res = 0;
  register int *data_p = data;
  register int pos = 0;

  for (register int j = 0; j < RUNS; j++) 
    for (register int i = 0; i < N; i++) 
      pos = (pos + i) & N;
      data_p[pos] = res;
    
  

  return res;


int main() 
  data = (int *)malloc(sizeof(int) * (N + 1));
  assert(data);

  for (int i = 0; i < N; i++) 
    data[i] = i;
  

  for (int i = 0; i < 10; i++) 
    PRINT_EXCECUTION_TIME("seqR", seqR());
    PRINT_EXCECUTION_TIME("seqW", seqW());
    PRINT_EXCECUTION_TIME("rndR", rndR());
    PRINT_EXCECUTION_TIME("rndW", rndW());
  

  return 0;

我使用gcc 6.5.0-O0 来防止优化,但得到的结果如下:

seqR time: 2538.010000 ms
seqW time: 2394.991000 ms
rndR time: 40625.169000 ms
rndW time: 46184.652000 ms
seqR time: 2411.038000 ms
seqW time: 2309.115000 ms
rndR time: 41575.063000 ms
rndW time: 46206.275000 ms

很容易理解,顺序访问比随机访问要快得多。但是,随机写入比随机读取慢而顺序写入比顺序读取快对我来说没有意义。什么原因会导致这种情况?

此外,我可以肯定地说seqR 的内存带宽是(20 * ((1 &lt;&lt; 27) - 1) * 4 * 1024 * 1024 * 1024)GB / (2.538)s = 4.12GB/s

【问题讨论】:

你用什么处理器来运行实验? 在我看来data_p[N] 可以在所有四个函数中访问。您可能想要分配 N+1 ints。 谢谢,应该是N + 1。我正在使用 Intel Xeon E5-2695 v4 Broadwell。 @zingdle:哦。与四核桌面相比,多核 Xeon 对单线程内存带宽不利是出了名的。不过,4GB/s 仍然低于我的预期,所以我仍然认为您在顺序函数中受 CPU 限制(而不是内存)。见Why is Skylake so much better than Broadwell-E for single-threaded memory throughput? @PeterCordes 是的,看起来原始代码受 CPU 限制。我将数组更改为volatile,删除register 并使用O3 重新编译。程序集告诉我它使用一些xmm 寄存器进行优化,但花费的时间与以前大致相同。如果我省略pos = (pos + i) &amp; N; 并使用data[i] 访问数组,则花费的时间减半。但是,我不能像以前那样直接比较随机/顺序内存访问时间。 【参考方案1】:

听起来很正常。所有 x86-64 CPU(以及大多数其他现代 CPU)都使用回写/写分配缓存,因此写入需要先读取一次,然后才能提交到缓存,以及最终的回写。

使用-O0 防止优化

由于您在所有本地人上都使用了register,这是罕见的情况之一,这不会使您的基准测试毫无意义。

不过,您可以在数组上使用volatile,以确保每个访问都按顺序进行,但如何实现则由优化器决定。

我可以肯定地说 seqR 的内存带宽是 (20 * ((1 &lt;&lt; 27) - 1) * 4 * 1024 * 1024 * 1024)GB / (2.538)s = 4.12GB/s

不,你的分子中有一个额外的因子 2^30 和 10^9。但是你做错了,无论如何都接近了正确的数字。

正确的计算是每秒RUNS * N * sizeof(int) / time 字节,或者除以 10^9 GB/s。或除以 2^30 以获取基本 2 GiB/s。内存大小通常以 GiB 为单位,但您可以选择带宽; DRAM 时钟速度通常为 1600 MHz,因此对于以 GB/s 为单位的理论最大带宽而言,base-10 GB = 10^9 当然是正常的。)

所以 4.23 GB/s(以 10 GB 为基础)。

是的,您首先初始化了数组,因此定时运行都不会触发页面错误,但如果 CPU 尚未预热到最大 turbo,我可能仍会使用第二次运行。

但请记住,这是未优化的代码。这就是你的未优化代码运行的速度,并没有告诉你你的内存有多快。它可能受 CPU 限制,而不是内存。

特别是其中有一个冗余的&amp; N 以匹配rndR/W 函数的CPU 工作。硬件预取可能能够跟上 4GB/s 的速度,但它仍然无法在每个时钟周期读取 1 个int

【讨论】:

@HadiBrais:同意,这就是我在回答中这么说的原因:P 但是保持它在那里可能有助于控制 seq 与 rnd 循环中指令计数之间的差异。 我从这个精彩的回答中学到了很多,谢谢!我只是想比较顺序/随机访问之间的性能差异,所以有多余的register&amp; NO0。但我仍然不确定为什么顺序写入比读取略快,因为它也使用回写/写入分配。 @zingdle: register 如果您要使用-O0 进行编译,则不是多余的。这是register 关键字有用的一次。使用 GCC,它会阻止变量保存在内存中,并将存储转发存储/重新加载延迟放入循环携带的依赖链中。 (看看生成的 asm;循环结构可能仍然是废话,但至少它没有将循环变量保存在内存中。) @zingdle:我认为顺序写入更快,因为存储缓冲区可以帮助隐藏偶尔出现的气泡。就像我说的那样,您的代码远没有成为实际内存/缓存带宽的瓶颈,因此硬件预取到 L2 可以轻松跟上。 (除非你的 CPU 是古老的)。您只会在页面边界处停顿。加载必须先完成,然后才能从无序的后端退出,但存储必须在 提交到 L1d 之前退出。 (存储缓冲区将缓存与推测执行隔离开来)。 我在 Haswell 上运行了代码。这些数字接近 OP 显示的(在 Broadwell E5 上),除了观察结果在多次运行中不成立,即 seqR/rndR 不一定比 seqW/rndW 快或慢.在许多运行中,差异在 1% 以内,但有时会更大一些。我想你是正确的。顺序循环可能受流水线限制(不是缓存或内存限制),随机循环似乎受至少 L1 上的填充缓冲区数量限制 (L1D_PEND_MISS.FB_FULL)。在这两种情况下,实现的 BW 都远小于最大单线程 BW。

以上是关于随机内存写入比随机内存读取慢?的主要内容,如果未能解决你的问题,请参考以下文章

为什么极度随机树比随机森林更随机?这个极度随机的特性有什么好处?在训练阶段极度随机数比随机森林快还是慢?

为啥从内存中插入的“随机”数字通常非常大?

下一代分布式消息系统Apache Kafka

为啥顺序写入比 HDD 上的随机写入快

计算机内存读取写入原理

内存映射文件的性能特点