【问题标题】:C++: Improving cache performance in a 3d arrayC++:提高 3d 数组中的缓存性能
【发布时间】:2012-03-30 23:30:49
【问题描述】:

我不知道如何在非常低的级别优化缓存性能,考虑缓存行大小或关联性。这不是你可以在一夜之间学会的东西。考虑到我的程序将在许多不同的系统和架构上运行,我认为无论如何都不值得。但是,总体而言,我可能可以采取一些步骤来减少缓存未命中。

这是我的问题的描述:

我有一个 3d 整数数组,表示空间点的值,例如 [x][y][z]。每个维度都是相同的大小,所以它就像一个立方体。从那我需要创建另一个 3d 数组,其中这个新数组中的每个值都是 7 个参数的函数:原始 3d 数组中的相应值,加上在空间中“触摸”它的 6 个索引。我暂时不担心立方体的棱角。

这就是我在 C++ 代码中的意思:

void process3DArray (int input[LENGTH][LENGTH][LENGTH], 
                     int output[LENGTH][LENGTH][LENGTH])
{
    for(int i = 1; i < LENGTH-1; i++)
        for (int j = 1; j < LENGTH-1; j++)
            for (int k = 1; k < LENGTH-1; k++)
            //The for loops start at 1 and stop before LENGTH-1
            //or other-wise I'll get out-of-bounds errors
            //I'm not concerned with the edges and corners of the 
            //3d array "cube" at the moment.
            {
                int value = input[i][j][k];

                //I am expecting crazy cache misses here:
                int posX = input[i+1] [j]   [k];
                int negX = input[i-1] [j]   [k];
                int posY = input[i]   [j+1] [k];
                int negY = input[i]   [j-1] [k];
                int posZ = input[i]   [j]   [k+1];
                int negZ = input[i]   [j]   [k-1];

                output [i][j][k] = 
                    process(value, posX, negX, posY, negY, posZ, negZ);
            }
}

但是,如果 LENGTH 足够大,当我获取 process 的参数时,我会得到大量的缓存未命中。有没有一种缓存友好的方式来做到这一点,或者有更好的方式来表示我的数据而不是 3d 数组?

如果您有时间回答这些额外的问题,我是否必须考虑 LENGTH 的价值?就像 LENGTH 是 20 vs 100 vs 10000 是不同的。另外,如果我使用整数以外的东西,比如可能是 64 字节的结构,我是否需要做其他事情?

@ildjarn:

抱歉,我认为生成我传递给process3DArray 的数组的代码并不重要。但如果是这样,我想知道为什么。

int main() {
    int data[LENGTH][LENGTH][LENGTH];
    for(int i = 0; i < LENGTH; i++)
        for (int j = 0; j < LENGTH; j++)
            for (int k = 0; k < LENGTH; k++)
                data[i][j][k] = rand() * (i + j + k);

    int result[LENGTH][LENGTH][LENGTH];
    process3DArray(data, result);
}

【问题讨论】:

  • “吨”是什么意思?你预计有多少?
  • 我真的不知道。我可能会遇到 posX、negX、posY 和 negY 的缓存未命中,但可能不会出现 posZ 和 negZ,因为它们具有更好的局部性。
  • 显示调用process3DArray的代码,特别是为inputoutput传递的数组是如何分配的。

标签: c++ caching


【解决方案1】:

这里有一个类似问题的答案:https://stackoverflow.com/a/7735362/6210(我的!)

优化多维数组遍历的主要目标是确保您访问该数组,以便您倾向于重用从前一个迭代步骤访问的缓存行。对于一次且仅一次访问数组的每个元素,您可以通过按内存顺序访问来做到这一点(就像您在循环中所做的那样)。

由于您执行的操作比简单的元素遍历(访问一个元素加上 6 个邻居)更复杂,因此您需要分解遍历,以免一次访问太多缓存行。由于缓存抖动主要是沿 jk 遍历,因此您只需修改遍历方式,以便一次访问块而不是一次访问行。

例如:

const int CACHE_LINE_STEP= 8;

void process3DArray (int input[LENGTH][LENGTH][LENGTH], 
                     int output[LENGTH][LENGTH][LENGTH])
{
    for(int i = 1; i < LENGTH-1; i++)
        for (int k_start = 1, k_next= CACHE_LINE_STEP; k_start < LENGTH-1; k_start= k_next; k_next+= CACHE_LINE_STEP)
        {
            int k_end= min(k_next, LENGTH - 1);

            for (int j = 1; j < LENGTH-1; j++)
                //The for loops start at 1 and stop before LENGTH-1
                //or other-wise I'll get out-of-bounds errors
                //I'm not concerned with the edges and corners of the 
                //3d array "cube" at the moment.
            {
                for (int k= k_start; k<k_end; ++k)
                {
                    int value = input[i][j][k];

                    //I am expecting crazy cache misses here:
                    int posX = input[i+1] [j]   [k];
                    int negX = input[i-1] [j]   [k];
                    int posY = input[i]   [j+1] [k];
                    int negY = input[i]   [j-1] [k];
                    int posZ = input[i]   [j]   [k+1];
                    int negZ = input[i]   [j]   [k-1];

                    output [i][j][k] = 
                        process(value, posX, negX, posY, negY, posZ, negZ);
                }
            }
        }
}

这样做是为了确保您不会通过以面向块的方式访问网格(实际上,更像是一种受缓存行大小限制的面向胖列的方式)来破坏缓存。它并不完美,因为列之间存在跨缓存行的重叠,但您可以对其进行调整以使其更好。

【讨论】:

  • 我很想看到一些证据证明这会有所不同,因为它只使用了 5 个不相交的内存部分。
【解决方案2】:

您已经拥有的最重要的事情是正确的。如果您使用的是 Fortran,那您就完全错了,但那是另一回事了。您正确的是,您正在沿着内存地址最接近的方向在内部循环中进行处理。单个内存提取(超出缓存)将拉入多个值,对应于一系列相邻的 k 值。在您的循环内,缓存将包含来自 i,j 的一些值;来自 i+/-1, j 和 i,j+/-1 的类似数字。因此,您基本上有五个不相交的内存部分处于活动状态。对于较小的 LENGTH 值,这些将只有 1 或 3 个内存段。根据缓存的构建方式,您的活动集中可以拥有比这么多不相交的内存部分。

我希望 process() 很小,并且是内联的。否则,这可能是微不足道的。此外,它还会影响您的代码是否适合指令缓存。

由于您对性能感兴趣,因此几乎总是最好初始化五个指针(对于值、posZ 和 negZ,您只需要一个),然后在循环中使用 *(p++)。

input[i+1] [j]   [k];

要求编译器生成 3 个加法和两个乘法,除非您有一个非常好的优化器。如果你的编译器对寄存器分配特别懒惰,你也会得到四次内存访问;否则一个。

*inputIplusOneJK++ 

要求一个添加和一个内存引用。

【讨论】:

  • 对不起,如果我误解了你(我不明白“活动集”和“寄存器分配”之类的词)。我是如何阅读您的答案的:我的代码现在不会在第三次 for 循环的每次迭代中出现缓存未命中,然后您继续描述一些小的优化。我几乎可以肯定这段代码不会导致大量指令缓存未命中。如果这是正确的,那么非常感谢您的回答。
  • 一个活动集是最近使用的内存段的集合——如果活动集足够小,它都适合缓存。寄存器分配是编译器决定哪些变量放在寄存器中,哪些留在内存中。是的,我是说你不应该有异常大量的缓存未命中。
猜你喜欢
  • 1970-01-01
  • 2018-09-04
  • 2015-06-17
  • 2014-07-28
  • 2011-09-24
  • 1970-01-01
  • 2010-12-27
  • 2012-08-13
  • 1970-01-01
相关资源
最近更新 更多