C语言 从寄存器移动到频繁访问的变量时,性能意外变慢

h6my8fg2  于 2023-08-03  发布在  其他
关注(0)|答案(2)|浏览(91)

我正在使用以下示例了解缓存的工作原理:

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>

typedef uint32_t data_t;
const int U = 10000000;   // size of the array. 10 million vals ~= 40MB
const int N = 100000000;  // number of searches to perform

int main() {
  data_t* data = (data_t*) malloc(U * sizeof(data_t));
  if (data == NULL) {
    free(data);
    printf("Error: not enough memory\n");
    exit(-1);
  }

  // fill up the array with sequential (sorted) values.
  int i;
  for (i = 0; i < U; i++) {
    data[i] = i;
  }

  printf("Allocated array of size %d\n", U);
  printf("Summing %d random values...\n", N);

  data_t val = 0;
  data_t seed = 42;
  for (i = 0; i < N; i++) {
    int l = rand_r(&seed) % U;
    val = (val + data[l]);
  }

  free(data);
  printf("Done. Value = %d\n", val);
  return 0;
}

字符串
使用perf record ./sum然后使用perf report找到的慢速随机访问循环的相关注解是

0.05 │       mov    -0x18(%rbp),%eax                                                                 ▒
  0.07 │       mov    -0x10(%rbp),%rcx                                                                 ▒
       │       movslq -0x20(%rbp),%rdx                                                                 ▒
  0.03 │       add    (%rcx,%rdx,4),%eax                                                               ▒
 95.39 │       mov    %eax,-0x18(%rbp)                                                                 ▒
  1.34 │       mov    -0x14(%rbp),%eax                                                                 ▒
       │       add    $0x1,%eax                                                                        ◆
       │       mov    %eax,-0x14(%rbp)


此时,-0x18持有val-0x10持有data-0x14持有i-0x20持有l。左栏中的数字显示该指令所花费的时间百分比。我预计add (%rcx, %rdx, 4), %eax行将占用最多的时间,因为它必须为data[l](即(%rcx, %rdx, 4))执行随机访问负载。这应该只在L1缓存中大约16 k/U = 0.16%的时间,因为我的L1缓存大小为64 k字节,或16 k整数。所以这次行动应该是缓慢的。相比之下,显然很慢的操作只是从寄存器%eax移动到val,该寄存器经常使用,因此它肯定在缓存中。有人能解释一下发生了什么事吗?

axr492tv

axr492tv1#

硬件性能计数器通常“责怪”等待缓慢结果的指令(store),而不是产生缓慢结果的指令。(微融合到load+add uop中的内存源add)。

和/或它们在高速缓存未命中加载之后归咎于下一指令,而不管数据依赖性。这被称为“打滑”或“偏斜”。例如,参见https://easyperf.net/blog/2018/08/29/Understanding-performance-events-skidhttps://www.brendangregg.com/perf.html
我对这种影响的原因的假设是,我认为Intel CPU在中断被引发时等待ROB中最旧的指令退出,也许是为了避免在高中断情况下饿死主线程。对于一个缓存未命中加载,它最终会停止无序执行,它将是ROB中最旧的,直到加载数据到达才能退出(因为x86的强有序内存模型不允许加载在此之前退出,即使它们已知是非故障的,unlike ARM)。因此,当“cycles”事件的计数器滴答下降到零并触发样本时,缓存未命中加载引退,并且程序顺序中的下一条指令得到该样本的“责备”。
对于要附加到特定指令的事件,如mem_load_retired.l3_miss,滑动更有问题,但英特尔PEBS避免了这种情况。在前面的段落中,我讨论了“cycles”事件,它在每个周期都发生,但是对于mem_load_retired.l3_miss,您可能会得到同样的结果,直到从L3切片得到反馈才能检测到。
在不停止的代码中,在同一个周期中全部退出的一组指令中的第一个或第二个可能会受到指责。CPU必须从流水线中的所有正在运行的指令中选择一条指令来进行指责。通过它引发中断的位置(非PEBS)或哪个指令地址进入PEBS缓冲区。
另请参阅Inconsistent perf annotate memory load/store time reporting,这是一个不太简单/明显的情况,但指责指令等待缓慢的结果是其中的关键部分。

svgewumm

svgewumm2#

我认为是这样的...
首先要注意的是,线int l = rand_r(&seed) % U;是不变的。也就是说,它总是被计算为相同的东西。因此,编译器将该值缓存在%rcx%rdx中。
考虑到这一点,让我们来看看这些线...

0.03 │       add    (%rcx,%rdx,4),%eax                                                               
 95.39 │       mov    %eax,-0x18(%rbp)

字符串
其中第一个将data[l]val相加,并将结果存储在%eax中。由于data[l]永远不会更改,因此它将始终从相同的内存位置加载值。此值可能会被硬件缓存。第二个是将和写入堆栈上的局部变量:val
正是将值写入堆栈花费了如此多的时间。这里使用的缓存类型可以是直写,因此每次该值更改时都会更新后备存储器。
是否需要在每次迭代时更新val?不,但这样做并没有错。

  • 更新 *

其他人都正确地指出并确定了我上面的解释中的一个基本缺陷,这使得它完全错误。请忽略它。:)

相关问题