代码之家  ›  专栏  ›  技术社区  ›  user1413793

阵列迭代中的CPU空间缓存位置

  •  5
  • user1413793  · 技术社区  · 7 年前

    我对一级缓存的理解是,内存提取会加载缓存线。假设缓存线大小为64字节,如果我访问地址处的内存 p ,它将从加载整个块 p p + 64 进入缓存。因此,最好从左到右(而不是从右到左)遍历数组,以最大化缓存位置。

    然而,我编写了示例C代码,该代码分配了一个一亿字符的数组,将随机值写入其中并求和(复制如下以供参考)。一个版本的代码从左到右求和,另一个版本的代码从右到左求和。当我对其进行基准测试时,我得到了非常相似的结果(其中“时钟周期”是根据 clock . 代码编译时没有进行任何优化。

    所以我的问题是:现代处理器除了“缓存读+64字节”之外,还能做些什么吗?它们会前后缓存吗?编译器能否“告诉”处理器代码正在向后迭代?

    作为参考,我正在运行 Mac OS X 10.13.3 使用 gcc-7 (Homebrew GCC 7.2.0_1) 7.2.0 以及具有64字节缓存线的x86-64 Intel处理器。

    基准:

    $ ./a.out
    Backward Iterating...took 150101 clock cycles
    
    $ ./a.out
    Forward Iterating...took 146545 clock cycles
    

    我预计前向迭代的速度会快64倍,因为每64个元素都应该是缓存命中,而对于后向迭代,每个元素都应该是缓存未命中。

    所以,我打电话给cachegrind。两者的缓存命中率几乎相同:

    # Left to right iteration
    ==21773==
    ==21773== I   refs:      4,006,996,067
    ==21773== I1  misses:            5,183
    ==21773== LLi misses:            3,019
    ==21773== I1  miss rate:          0.00%
    ==21773== LLi miss rate:          0.00%
    ==21773==
    ==21773== D   refs:      1,802,393,260  (1,401,627,925 rd   + 400,765,335 wr)
    ==21773== D1  misses:        3,153,101  (    1,588,104 rd   +   1,564,997 wr)
    ==21773== LLd misses:        3,004,885  (    1,440,660 rd   +   1,564,225 wr)
    ==21773== D1  miss rate:           0.2% (          0.1%     +         0.4%  )
    ==21773== LLd miss rate:           0.2% (          0.1%     +         0.4%  )
    ==21773==
    ==21773== LL refs:           3,158,284  (    1,593,287 rd   +   1,564,997 wr)
    ==21773== LL misses:         3,007,904  (    1,443,679 rd   +   1,564,225 wr)
    ==21773== LL miss rate:            0.1% (          0.0%     +         0.4%  )
    
    # Right to left iteration
    ==21931==
    ==21931== I   refs:      4,006,996,453
    ==21931== I1  misses:            5,198
    ==21931== LLi misses:            3,045
    ==21931== I1  miss rate:          0.00%
    ==21931== LLi miss rate:          0.00%
    ==21931==
    ==21931== D   refs:      1,802,393,428  (1,401,628,038 rd   + 400,765,390 wr)
    ==21931== D1  misses:        3,153,113  (    1,588,079 rd   +   1,565,034 wr)
    ==21931== LLd misses:        3,135,505  (    1,571,219 rd   +   1,564,286 wr)
    ==21931== D1  miss rate:           0.2% (          0.1%     +         0.4%  )
    ==21931== LLd miss rate:           0.2% (          0.1%     +         0.4%  )
    ==21931==
    ==21931== LL refs:           3,158,311  (    1,593,277 rd   +   1,565,034 wr)
    ==21931== LL misses:         3,138,550  (    1,574,264 rd   +   1,564,286 wr)
    ==21931== LL miss rate:            0.1% (          0.0%     +         0.4%  )
    

    代码:

    #include <stdint.h>
    #include <time.h>
    #include <stdio.h>
    #include <stdlib.h>
    
    #define BUF_SIZE 100000000
    
    int main() {
      srand(time(NULL));
      uint8_t *buf1 = (uint8_t *)malloc(BUF_SIZE);
      // Fill the buf with random data
      for (size_t i = 0; i < BUF_SIZE; ++i) {
        buf1[i] = rand();
      }
    
    #ifdef BACKWARDS
      printf("Backward Iterating...");
    #else
      printf("Forward Iterating...");
    #endif
    
      uint64_t sum = 0;
      clock_t start = clock();
    #ifdef BACKWARDS
      for (size_t i = BUF_SIZE - 1; i != ~0; --i) {
    #else
      for (size_t i = 0; i < BUF_SIZE; ++i) {
    #endif
        sum += buf1[i];
      }
      clock_t end = clock();
      printf("took %lu clock cycles\n", end - start);
      printf("sum: %llu\n", sum);
      free(buf1);
    }
    
    2 回复  |  直到 7 年前
        1
  •  6
  •   Leeor    7 年前

    要扩展上一个答案:

    加载完整的缓存线粒度意味着向前或向后并不重要,一旦到达缓存线的一侧,就可以得到全部缓存线。当然,这只适用于可缓存的加载和memtypes(+仍在缓冲区中时可能被命中的流情况)。

    然而,这并不是全部情况。现代CPU使用的硬件预取器在处理空间局部性方面非常好——这些预取器可以按照您正在进行的相同方向预取额外的缓存线,从而提高粒度。现有的预取器取决于您使用的确切体系结构,但常见的预取器包括下一行、相邻行(+/-1行)、下一行流或基于IP的跨步。更多信息 here .

    这些预取器应该是对称的,但我们不能确定(确切的细节没有透露),它们在不同的方向上可能有不同的机会或阈值。

    另一点是,cachegrind只是一个缓存模拟,它不包括预取之类的效果,甚至不能为精确的缓存建模(大小应该可以,但替换策略和其他微体系结构细节不能保证相同),所以你看不到全部效果。使用性能计数器查看实际硬件行为可能更好。

        2
  •  4
  •   user3386109    7 年前

    如果我访问地址处的内存 p ,它将从加载整个块 p p + 64 进入缓存。

    不完全是这样。处理器加载的是包含 p . 例如,如果 p 为0x1234,则加载缓存线0x1200到0x123F。因此,在阵列中向后扫描与向前扫描没有太大区别。

    推荐文章