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

#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))执行随机访问加载.这应该只在大约16k/U=0.16%的情况下在L1缓存中,因为我的L1缓存的大小是64k字节,或16k整数.因此,这一操作应该是缓慢的.相反,看起来很慢的操作只是从寄存器%eax移到val,该寄存器被如此频繁地使用,以至于它肯定在高速缓存中.有人能解释一下这是怎么回事吗?

推荐答案

HW performance counters usually "blame" the instruction waiting for the slow result (the 100), not the instruction that was slow to produce it.(微融合成加载+加法微操作的存储源add).

和/或它们在高速缓存未命中加载之后责怪下一条指令,而不考虑数据相关性.这被称为"打滑"或"歪斜".请参见示例https://easyperf.net/blog/2018/08/29/Understanding-performance-events-skidhttps://www.brendangregg.com/perf.html

我对这种影响的原因的假设是,我认为Intel CPU等待ROB中最旧的指令在中断引发时退役,这可能是为了避免在高中断情况下导致主线程饥饿.对于最终导致无序执行停滞的缓存未命中加载,它将是ROB中最旧的,在加载数据到达之前无法退出(因为x86的S强序内存模型不允许在此之前退出加载,即使已知它们没有错误,unlike ARM).因此,当"Cycle"事件的计数器滴答到零并触发一个样本时,缓存未命中加载退出,程序顺序中的下一条指令得到该样本的"责备".

对于打算附加到特定指令的事件,如mem_load_retired.l3_miss,打滑更有问题,但英特尔PEB避免了这一点.在前面的段落中,我谈到了"周期"事件,它在停滞时每个周期都在滴答作响,但您可能会得到相同的mem_load_retired.l3_miss,直到收到L3切片的回复才能被检测到.

在不会停顿的代码中,一组指令中的第一个或第二个指令在同一周期中全部退役可能会受到指责.CPU必须从流水线中正在运行的所有指令中 Select 一条指令进行指责.通过其引发中断的位置(非PEB)或PEBS缓冲器中的指令地址.


另请参阅Inconsistent `perf annotate` memory load/store time reporting,这是一个不那么简单/明显的情况,但将原因归咎于等待缓慢结果的指令是其中的关键部分.

C++相关问答推荐

malloc实现:判断正确的分配对齐

使用C时,Windows CMD中的argc参数是否包含重定向命令?

获取每个循环迭代结束时的当前时间

如何在C++中处理按键

用gcc-msse 2编译的C程序包含AVX 1指令

在Linux上使用vscode和lldb调试用Makefile编译的c代码

仅从限制指针参数声明推断非混叠

Wcstok导致分段故障

不确定如何处理此编译错误

静态初始化顺序失败是否适用于C语言?

生产者消费者计数器意外输出的C代码

在printf()中用%.*S格式填充长度为0的字符串是否会调用任何UB?如果是,是哪一个?

我编写这段代码是为了判断一个数字是质数、阿姆斯特朗还是完全数,但由于某种原因,当我使用大数时,它不会打印出来

即使客户端不发送数据,也会发生UNIX套接字读取

在C中定义函数指针?

C 语言中霍尔分区的快速排序算法

快速准确计算double的小数指数

仅使用其内存地址取消引用 C 中的 struct

为什么使用 C 引用这个 char 数组会导致 Stack smasing?

我们可以在不违反标准的情况下向标准函数声明添加属性吗?