【问题标题】:Hitting CPU cache for better performance in C++命中 CPU 缓存以获得更好的 C++ 性能
【发布时间】:2021-02-19 07:31:14
【问题描述】:

CPU 缓存是否首选连续缓冲区?

我正在尝试编写一个对图像执行多项操作的应用程序(一些非常局部的操作,例如移位、导数和一些结果之间的减法)。 我有一个很大的结果缓冲区(每个计算都有一个图像的形状,所以我从分配 X * 图像形状字节开始)

我应该怎么做才能最大限度地提高 CPU 缓存命中率?

【问题讨论】:

  • CPU 缓存首选连续缓冲区吗? 是的。缓存是您拥有的最快的内存。如果您可以保留在缓存中,那么您将尽可能快,至少在读写方面是这样。
  • 您可能还想考虑使用 GPU。
  • 不只是缓存,还有预取器。拥有可预测且清晰的数据访问模式有助于 CPU 在您需要之前预取数据。当 CPU 可以为您准备好数据时,仅此一项就可以节省数千个等待 RAM 的周期。
  • 如果图像操作比单纯的减法更复杂,例如平滑/过滤,应该考虑多线程
  • 尝试将您的图像分成小方块。在该正方形上执行尽可能多的功能。然后移动到下一个方块。也许按行更好;只有基准测试会告诉我们。换句话说,尽可能在数据在缓存中的时候做,防止缓存重新加载。

标签: c++ cpu-cache


【解决方案1】:
#ifdef _MSC_VER
        _declspec(align(64))    unsigned char  block_hashfp;
#else
        __attribute__((aligned(64))) unsigned char  block_hashfp;
#endif

将命中二级缓存

【讨论】:

  • 请说明将内存对齐到 64 位边界将如何确保内存在缓存中。
  • 请注意,现在有一种标准方法可以找到对齐std::hardware_destructive_interference_size
  • @Mgetz 在他们的示例中打印 64。但为什么他们在谈论 level 1 缓存 idk。 64 无论如何都可以工作
  • @АлексейНеудачин 因为对于该功能的直接预期目的,L1 将成为目标。因为那是最初发生虚假共享的地方。事实上,它恰好与缓存行大小一致,这为错误共享的工作方式提供了一个很好的便利。
【解决方案2】:

当然,处理连续数组更有可能命中缓存。
您可能希望以连续的方式排列数据:
例如:

#include <iostream>
#include <cstdint>

const int SIZE = 3;

int main(){
    uint8_t buffer_2d[SIZE][SIZE];
    uint8_t* buffer_1d = reinterpret_cast<uint8_t*>(buffer_2d); // or just do uint8_t buffer_1d[SIZE*SIZE];
    const auto base = &(buffer_2d[0][0]);
    for (int y=0;y<SIZE;++y){
        for (int x=0;x<SIZE;++x){
            std::cout << "x: " << x << " y: " << y << " offset for [x][y]: " << &(buffer_2d[x][y]) - base << " offset for [y][x]: " << &(buffer_2d[y][x]) - &(buffer_2d[0][0]) << " offset for [y*SIZE+x]: " << &(buffer_1d[y*SIZE+x]) - base <<  std::endl;
        }
    }
    return 0;
}

将数组视为人类自然的[x][y] 数组不会有效,因为数据不是以这种方式对齐的,有效的方法是使用[y][x] 或将数组作为一维数组处理并处理索引为y*LINE_SIZE + x
下面是这个测试的输出,准确地表明了这一点:

x: 0 y: 0 offset for [x][y]: 0 offset for [y][x]: 0 offset for [y*SIZE+x]: 0
x: 1 y: 0 offset for [x][y]: 3 offset for [y][x]: 1 offset for [y*SIZE+x]: 1
x: 2 y: 0 offset for [x][y]: 6 offset for [y][x]: 2 offset for [y*SIZE+x]: 2
x: 0 y: 1 offset for [x][y]: 1 offset for [y][x]: 3 offset for [y*SIZE+x]: 3
x: 1 y: 1 offset for [x][y]: 4 offset for [y][x]: 4 offset for [y*SIZE+x]: 4
x: 2 y: 1 offset for [x][y]: 7 offset for [y][x]: 5 offset for [y*SIZE+x]: 5
x: 0 y: 2 offset for [x][y]: 2 offset for [y][x]: 6 offset for [y*SIZE+x]: 6
x: 1 y: 2 offset for [x][y]: 5 offset for [y][x]: 7 offset for [y*SIZE+x]: 7
x: 2 y: 2 offset for [x][y]: 8 offset for [y][x]: 8 offset for [y*SIZE+x]: 8

最后两个结果使用完全相同的语义,第一个将让编译器发出计算,但性能应该相同。

此外,一旦您的数据被正确排列,根据您对数据的处理方式,您可能想要使用 OpenCL 或其他东西并利用 GPU 或 SIMD,如果可以用 SIMD(单指令多数据)代码。

【讨论】:

    猜你喜欢
    • 2013-03-24
    • 2012-06-22
    • 2011-01-30
    • 1970-01-01
    • 2011-02-07
    • 1970-01-01
    • 2013-11-26
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多