为什么2048x2048与2047x2047阵列乘法相比会产生巨大的性能影响?


127

我正在做一些矩阵乘法基准测试,如前面在MATLAB为什么矩阵乘法中这么快提到的那样

现在,我又遇到了另一个问题,当将两个2048x2048矩阵相乘时,C#与其他矩阵有很大的不同。当我尝试仅乘以2047x2047矩阵时,这似乎很正常。也添加了一些其他内容进行比较。

1024x1024-10秒。

1027x1027-10秒。

2047x2047-90秒。

2048x2048-300秒。

2049x2049-91秒。(更新)

2500x2500-166秒

2k x 2k情况相差三分半钟。

使用2dim数组

//Array init like this
int rozmer = 2048;
float[,] matice = new float[rozmer, rozmer];

//Main multiply code
for(int j = 0; j < rozmer; j++)
{
   for (int k = 0; k < rozmer; k++)
   {
     float temp = 0;
     for (int m = 0; m < rozmer; m++)
     {
       temp = temp + matice1[j,m] * matice2[m,k];
     }
     matice3[j, k] = temp;
   }
 }

23
对于高级C编程或OS设计课程来说,这将是一个很好的考试问题;-)
Dana the Sane

您是否尝试过测试多维[,]和锯齿[] []数组以及32位和64位数组?我只测试了几次,但锯齿状与您的结果似乎更加一致,但锯齿状的64位很高,我不知道jit中是否有适用于这种情况的试探法,或者它的缓存是否如先前建议的那样相关。如果您需要GPGPU解决方案,请访问research.microsoft.com/en-us/projects/accelerator,它应该与其他文章中的时代相比具有竞争力。
克里斯(Kris)

有点天真的问题,但是将两个平方矩阵相乘涉及多少个运算(加/乘)?
尼克T

Answers:


61

这可能与L2缓存中的冲突有关。

matice1上的缓存未命中不是问题,因为它们是按顺序访问的。但是对于matice2,如果一个完整的列适合L2(即,当您访问matice2 [0,0],matice2 [1,0],matice2 [2,0]等时,什么都不会被驱逐),那么就没有问题了用matice2高速缓存未命中。

现在更深入地了解缓存的工作原理,如果变量的字节地址为X,则其缓存行将为(X >> 6)&(L-1)。其中L是缓存中的缓存行总数。L始终是2的幂。这六个事实来自于2 ^ 6 == 64字节是高速缓存行的标准大小。

现在这是什么意思?好吧,这意味着如果我有地址X和地址Y并且(X >> 6)-(Y >> 6)可被L整除(即2的大幂),它们将存储在同一缓存行中。

现在回到您的问题,2048和2049有什么区别,

当您的大小为2048时:

如果采用&matice2 [x,k]和&matice2 [y,k],则差(&matice2 [x,k] >> 6)-(&matice2 [y,k] >> 6)将被2048 * 4除(大小的浮动)。因此2的大方。

因此,根据L2的大小,您将有很多缓存行冲突,并且仅利用L2的一小部分来存储列,因此您实际上将无法在缓存中存储完整的列,因此会导致性能下降。

当size为2049时,差异为2049 * 4(不是2的幂),因此您的冲突将更少,并且您的列将安全地放入缓存中。

现在,要检验此理论,您可以做一些事情:

像这样的matice2 [razmor,4096]分配数组matice2数组,并以razmor = 1024、1025或任何大小运行,与以前相比,您会发现性能很差。这是因为您强制对齐所有列以使其相互冲突。

然后尝试matice2 [razmor,4097]并以任意大小运行它,您应该会看到更好的性能。


您在最后两段中是否犯了错误?两次尝试完全相同。:)
Xeo

缓存关联性也起作用。
本杰克逊

20

可能是缓存效果。矩阵的大小是2的幂,并且缓存大小也是2的幂,您最终只能使用L1缓存的一小部分,从而大大降低了速度。朴素矩阵乘法通常受将数据提取到缓存中的需求所限制。使用平铺的优化算法(或忽略缓存的算法)专注于更好地利用L1缓存。

如果您计时其他对(2 ^ n-1,2 ^ n),我希望您会看到类似的效果。

为了更全面地解释,在访问matice2 [m,k]的内部循环中,matice2 [m,k]和matice2 [m + 1,k]可能彼此偏移2048 * sizeof(float)并因此映射到L1缓存中的相同索引。使用N路关联缓存时,通常所有这些缓存都有1-8个缓存位置。因此,几乎所有这些访问都将触发L1缓存逐出,并从较慢的缓存或主存储器中获取数据。


+1。听起来可能。必须注意缓存的关联性。
Macke,

16

这可能与cpu缓存的大小有关。如果矩阵的两行不适合,那么您将失去在RAM中交换元素的时间。额外的4095个元素可能足以防止行适合。

在您的情况下,用于2047 2d矩阵的2行位于16KB的内存范围内(假设32位类型)。例如,如果您有一个64KB的L1高速缓存(最接近总线上的cpu),则一次可以容纳至少4行(2047 * 32)行。对于较长的行,如果需要任何填充将行对推到16KB以上,则情况开始变得混乱。同样,每次您“丢失”缓存时,从另一个缓存或主内存中交换数据都会延迟时间。

我的猜测是,使用不同大小的矩阵所看到的运行时间差异会受到操作系统如何充分利用可用缓存的影响(有些组合是有问题的)。当然,这全是我的简化。


2
但他不太可能拥有16.7 MB的CPU缓存
MarinoŠimić'11

我将结果更新为2049x2049-91秒。如果是“缓存问题”,那应该不是300+秒吗?
沃尔夫,

@Marino答案已更新为考虑到这一点。
桑达纳

1
我觉得这些解释都无法充分解决引起问题的各种稀疏大小的新细节,而其他介于两者之间的影响不受影响。
肯·罗科特

2
我认为这种解释是不正确的。问题在于当大小为2的幂时,由于高速缓存行冲突而导致无法充分利用高速缓存容量。另外,操作系统实际上与高速缓存无关,因为不是由OS来决定高速缓存和退出哪种内容,而是全部在硬件上。操作系统与数据对齐有关,但在这种情况下,所有内容都与C#如何决定分配数据以及如何在内存中表示2D数组有关,操作系统与它无关。
zviadm 2011年


5

假设时间在更大的尺寸上减少了,难道不是缓存冲突的可能性更大,尤其是对于有问题的矩阵尺寸使用2的幂的情况?我不是缓存问题方面的专家,但是有关缓存相关性能问题的出色信息这里是


有关高速缓存关联性的链接的第5节似乎特别适用。
达纳(Dana the Sane)

4

当您matice2垂直访问数组时,它将在高速缓存中进行更多的交换。如果对角镜像该数组,以便可以使用[k,m]而不是访问它[m,k],则代码将运行得更快。

我测试了1024x1024矩阵,它的速度大约是它的两倍。对于2048x2048矩阵,速度要快十倍左右。


这并不能解释为什么2049年比2048
Macke

@Macke:这是因为它在内存缓存中通过了一些限制,因此有更多的缓存未命中。
2011年

为什么要下票?如果您不说自己的想法是错误的,那将无法改善答案。
2011年

另一个没有任何解释的下降式投票...是我的答案中“大概”,“猜测”和“应该”过少,就像得到最高投票的答案一样吗?
2011年

4

缓存别名

缓存颠簸,如果我能创造一个学期的话,那就。

高速缓存通过使用低阶位建立索引并使用高阶位进行标记来工作。

想象一下您的缓存有4个字,矩阵是4 x4。访问一列并且该行的长度为2的幂时,内存中的每个列元素都将映射到相同的缓存元素。

对于这个问题,二乘一的幂实际上是最佳的。每个新的列元素将映射到下一个缓存插槽,就像按行访问一样。

在现实生活中,标签覆盖了多个顺序增加的地址,这些地址将在一行中缓存几个相邻的元素。通过偏移每个新行映射到的存储桶,遍历该列不会替换先前的条目。当遍历下一列时,整个高速缓存将充满不同的行,适合该高速缓存的每一行部分将命中几列。

由于高速缓存要比DRAM快得多(主要是由于位于芯片上),因此命中率至关重要。


2

您似乎已达到缓存大小限制,或者在计时方面存在一些可重复性问题。

无论问题是什么,您都根本不应该自己用C#编写矩阵乘法,而应该使用BLAS的优化版本。在任何现代机器上,矩阵的大小应在不到一秒的时间内相乘。


1
我知道BLAS,但任务不是使它尽可能快,而是用各种语言编写和测试。对我来说,这是一个非常奇怪的问题,而Iam真的很好奇为什么结果会如此。

3
@Wolf对于要花一秒钟的时间是90秒还是300秒,我很难感到兴奋。
David Heffernan

4
学习某件作品的最佳方法是自己编写并查看如何改进其实现。这是(希望)沃尔夫在做什么。
卡勒姆·罗杰斯

@Callum Rogers,表示同意。这就是我了解缓冲区大小在文件复制操作中的重要性的方式。
凯利·法文

1

有效利用缓存层次结构非常重要。您需要确保多维数组的数据排列合理,可以通过平铺来完成。为此,您需要将2D数组与索引机制一起存储为1D数组。传统方法的问题在于,尽管同一行中的两个相邻数组元素在内存中彼此相邻,但是同一列中的两个相邻元素将被分隔开内存中 W个元素,其中W是列数。平铺可产生多达十分之一的性能差异。


嗯-仍然声明为2D的数组(float [,] matice = new float [rozmer,rozmer];)仅在RAM中分配为一维数组,并且在后台进行行/步距计算。那么,为什么将其声明为1D并进行手动行/步距计算会更快?您是说sol'n分配了一个大数组作为较小的tile数组,每个小tile都可以放入大数组无法容纳的缓存中?
埃里克·M

1
如果您的库或正在使用的任何工具都进行了平铺,则无需这样做。但是,如果要在C / C ++中使用传统的2D数组,则平铺将提高性能。
Arlen

0

我怀疑这是所谓的“ 顺序淹没 ”的结果 ”的结果。这是因为您正在尝试遍历略大于缓存大小的对象列表,因此必须从ram完成对列表(数组)的每个请求,并且不会获得单个缓存击中。

在您的情况下,您要遍历2048个索引的数组2048次,但您只有2047个空间(可能是由于数组结构的一些开销),因此,每次访问数组pos时,都需要获取此数组pos。从公羊。然后将其存储在缓存中,但是在再次使用之前,将其转储。因此,缓存实际上是无用的,从而导致更长的执行时间。


1
不正确 2049比2048快,这驳斥了您的主张。
Macke,

@Macke:那是完全有可能的。但有一个轻微的机会,在他的处理器中使用的高速缓存策略仍有可能使这descision。它的可能性很小,但并非不可想象。
Automatico
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.