如何对此代码段进行内存阻塞

Posted

技术标签:

【中文标题】如何对此代码段进行内存阻塞【英文标题】:how to do memory blocking for this code snippet 【发布时间】:2022-01-08 15:22:08 【问题描述】:

我有这段代码,我正在尝试使用缓存一致性方法来优化它,比如缓存阻塞的时间和空间局部性。 (https://www.intel.com/content/www/us/en/developer/articles/technical/cache-blocking-techniques.html)

void randFunction1(int *arrayb, int dimension)


    int i, j;

    for (i = 0; i < dimension; ++i)

        for (j = 0; j < dimension; ++j) 

            arrayb[j * dimension+ i] = arrayb[j * dimension+ i] || arrayb[i * dimension+ j];

        

这就是我优化它的方式,但有人告诉我它似乎没有使用内存阻塞技术。

for (int i = 0; i < dimension; ++i)
        int j = i;

        for (; j < dimension; ++j)
        
            //access 2 times 
            arrayb[j * dimension+ i] = arrayb[j * dimension+ i] || arrayb[i * dimension+ j]; 
            arrayb[i * dimension+ j] = arrayb[i * dimension+ j] || arrayb[j * dimension + i]; 
        

    

有人能告诉我如何在这段示例代码中使用缓存阻塞(对较小的切片使用局部性)吗?感谢您的帮助!

【问题讨论】:

内存阻塞是什么意思? @S.M.抱歉,我的意思是缓存阻塞:) 见en.wikipedia.org/wiki/Loop_nest_optimization 【参考方案1】:

我认为你对缓存阻塞有一个根本的误解,误解了你被要求做的事情,或者让你做的人不理解。我也不愿意给你完整的答案,因为这听起来像是一个家庭作业问题的人为例子。

这个想法是阻止/平铺/窗口化您正在操作的数据,因此您正在操作的数据在您对其进行操作时会保留在缓存中。要有效地做到这一点,您需要知道缓存的大小和对象的大小。你没有给我们足够的细节来知道这些答案,但我可以做一些假设来说明你如何使用上面的代码来做到这一点。

首先,数组是如何在内存中布局的,以便我们以后可以引用它。假设维度是 3。

这意味着我们有一个网格布局,其中 i 是第一个数字,j 是第二个数字...

[0,0][0,1][0,2]
[1,0][1,1][1,2]
[2,0][2,1][2,2]

这真的在记忆中像:

[0,0][0,1][0,2][1,0][1,1][1,2][2,0][2,1][2,2]

我们也可以将其视为一维数组,其中:

[0,0][0,1][0,2][1,0][1,1][1,2][2,0][2,1][2,2]
[ 0 ][ 1 ][ 2 ][ 3 ][ 4 ][ 5 ][ 6 ][ 7 ][ 8 ]

如果我们的缓存行可以容纳 3 个这样的家伙,那么就会有 3 个“块”。 0-2、3-5 和 6-8。如果我们按顺序访问它们,就会发生阻塞(假设数组索引 0 的字节对齐正确......但现在让我们保持简单 - 无论如何这可能已经处理好了)。也就是当我们访问 0 时,然后 0、1 和 2 被加载到缓存中。接下来我们访问 1,它已经在那里了。然后2,已经在那里了。然后3,将3、4、5加载到缓存中,以此类推。

让我们看一下原始代码。

arrayb[j * dimension+ i] = arrayb[j * dimension+ i] || arrayb[i * dimension+ j];

让我们只做几次迭代,但取出索引变量并用它们的值替换它们。我将使用 ^ 指向您访问的索引和 |显示我们想象中的缓存行的位置。

arrayb[0] = arrayb[0] || arrayb[0]
[ 0 ][ 1 ][ 2 ] | [ 3 ][ 4 ][ 5 ] | [ 6 ][ 7 ][ 8 ]
  ^

arrayb[3] = arrayb[3] || arrayb[1]
[ 0 ][ 1 ][ 2 ] | [ 3 ][ 4 ][ 5 ] | [ 6 ][ 7 ][ 8 ]
       ^            ^ 

arrayb[6] = arrayb[6] || arrayb[2]
[ 0 ][ 1 ][ 2 ] | [ 3 ][ 4 ][ 5 ] | [ 6 ][ 7 ][ 8 ]
            ^                         ^ 

arrayb[1] = arrayb[1] || arrayb[3]
[ 0 ][ 1 ][ 2 ] | [ 3 ][ 4 ][ 5 ] | [ 6 ][ 7 ][ 8 ]
       ^            ^ 

因此,除了第一次迭代之外,您会看到 每次 都在整个地方跳跃。

我想你注意到你正在执行的操作是合乎逻辑的。这意味着您不必在循环中保留原始操作顺序,因为您的答案将是相同的。那是你先做arrayb[1] = arrayb[1] || arrayb[3]还是先做arrayb[3] = arrayb[3] | arrayb[1]都没关系。

在您提出的解决方案中,您可能认为自己做得更好一些,因为您注意到在第二次和第四次迭代中我们访问相同索引的模式(只是翻转我们正在读取和写入的位置)但您没有完全调整循环,所以实际上你只做了两倍的工作。

0 = 0 || 0
0 = 0 || 0
3 = 3 || 1
1 = 1 || 3
6 = 6 || 2
2 = 2 || 6
1 = 1 || 3
3 = 3 || 1
4 = 4 || 4
4 = 4 || 4
7 = 7 || 5
5 = 5 || 7
2 = 2 || 6
6 = 6 || 2
5 = 5 || 7
7 = 7 || 5
8 = 8 || 8
8 = 8 || 8

如果你解决了双重工作,你就在路上,但你并没有真的使用阻塞策略。老实说,你不能。这几乎就像这个问题被设计成非现实世界并故意导致缓存问题一样。您的示例的问题是您使用的单个数组只能成对(两次)访问相同的内存位置。除了它们的交换之外,它们从未被重复使用。

您可以在某种程度上优化某些访问,但您总是会遇到跨越边界的多数集合。我认为这是您被要求做的事情,但这不是一个很好的示例问题。如果我们牢记您的数组中的内存实际上是如何被访问并且从未真正被重用的,那么增加示例的大小就会变得非常明显。

假设维度是 8 并且您的缓存足够大以容纳 16 个项目(x86_64 可以在缓存行中容纳 16 个整数)。那么最佳访问分组将是所有索引都落在 0-15、16-31、32-47 或 48-63 内的操作。数量不多。

不跨越缓存线:

0 = 0 || 0
1 = 1 || 8
8 = 8 || 1
9 = 9 || 9

18 = 18 || 18
19 = 19 || 26
26 = 26 || 19
27 = 27 || 27

36 = 36 || 36
37 = 37 || 44
44 = 44 || 37

54 = 54 || 54
55 = 55 || 62
62 = 62 || 55
63 = 63 || 63

总是越过缓存线:

2 = 2 || 16
3 = 3 || 24
4 = 4 || 32
5 = 5 || 40
6 = 6 || 48
7 = 7 || 56
10 = 10 || 17
11 = 11 || 25
12 = 12 || 33
13 = 13 || 41
14 = 14 || 49
15 = 15 || 57
16 = 16 || 2
17 = 17 || 10
20 = 20 || 34
21 = 21 || 42
22 = 22 || 50
23 = 23 || 58
24 = 24 || 3
25 = 25 || 11
28 = 28 || 35
29 = 29 || 43
30 = 30 || 51
31 = 31 || 59
32 = 32 || 4
33 = 33 || 12
34 = 34 || 20
35 = 35 || 28
38 = 38 || 52
39 = 39 || 60
40 = 40 || 5
41 = 41 || 13
42 = 42 || 21
43 = 43 || 29
45 = 45 || 45
46 = 46 || 53
47 = 47 || 61
48 = 48 || 6
49 = 49 || 14
50 = 50 || 22
51 = 51 || 30
52 = 52 || 38
53 = 53 || 46
56 = 56 || 7
57 = 57 || 15
58 = 58 || 23
59 = 59 || 31
60 = 60 || 39
61 = 61 || 47

这真的很糟糕,因为项目的数量超过了缓存中的数量。你现在唯一希望保存的就是你注意到的模式,在这种模式下你可以做一半的内存访问,虽然很聪明,但不是阻塞/平铺。

您提供的链接对于说明缓存阻塞同样糟糕。它不能很好地描述循环中实际发生的事情,但至少它会尝试。

他们将内部循环平铺以使内存访问更加本地化,​​我认为这是您被要求做的,但遇到了一个无法应用的问题。

闻起来像你的老师打算给你 2 或 3 个数组,但不小心只给了你一个。它非常接近矩阵乘法,但缺少一个内部循环和另外两个数组。

【讨论】:

以上是关于如何对此代码段进行内存阻塞的主要内容,如果未能解决你的问题,请参考以下文章

内存段是如何划分的

关于线程任务的一些思考

Linux从头学09:x86 处理器如何进行-层层的内存保护?

测量代码段的 Java 执行时间、内存使用和 CPU 负载

Linux从头学03:如何告诉 CPU,代码段数据段栈段在内存中什么位置?

为什么代码段会造成内存泄漏?