【发布时间】:2017-05-06 03:25:47
【问题描述】:
我正在研究缓存未命中如何影响计算速度。我知道有很多算法可以更好地使两个矩阵相乘(甚至简单地交换下面的两个循环也会有所帮助),但请考虑以下代码:
float a[N][N];
float b[N][N];
float c[N][N];
// ...
{
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
float sum = 0.0;
for (int k = 0; k < N; k++) {
sum = sum + a[i][k] * b[k][j];
}
c[i][j] = sum;
}
}
}
我已经为N 的许多值重新编译了这段代码,并测量了运行它的时间。我预计会在N=1250 附近发现时间突然增加,此时矩阵c 不再适合缓存(c 的大小然后是1250*1250*sizeof(float)=6250000,或大约 6MB,这是我的 L3 的大小缓存)。
确实,总体趋势是,在那之后,平均时间与之前的推断时间相比大约是三倍。但是N%8 的值似乎对结果有很大的影响。例如:
1601 - 11.237548
1602 - 7.679103
1603 - 12.216982
1604 - 6.283644
1605 - 11.360517
1606 - 7.486021
1607 - 11.292025
1608 - 5.794537
1609 - 11.469469
1610 - 7.581660
1611 - 11.367203
1612 - 6.126014
1613 - 11.730543
1614 - 7.632121
1615 - 11.773091
1616 - 5.778463
1617 - 11.556687
1618 - 7.682941
1619 - 11.576068
1620 - 6.273122
1621 - 11.635411
1622 - 7.804220
1623 - 12.053517
1624 - 6.008985
一段时间以来,我认为这可能是对齐问题 - 当N%8==0 时,任何矩阵的行都对齐到 32 个字节(第一个问题 - 为什么特别是 32 个字节?SSE 指令,例如 movaps 可以在 16B 上工作对齐的数据)。
另一个想法是,这可能与缓存关联性有关(在我的机器上,L1 和 L2 为 8 路,L3 为 12 路)。
但后来我注意到对于N 的某些值,例如1536,会出现意想不到的尖峰(即使在这些情况下对齐应该很好 - 1536==256*6,关联性也不是问题 - 1536==128*12==192*8)。例如:
1504 - 4.644781
1512 - 4.794254
1520 - 4.768555
1528 - 4.884714
1536 - 7.949040
1544 - 5.162613
1552 - 5.083331
1560 - 5.388706
时间非常一致,因此处理器负载峰值不是问题。我在打开优化的情况下编译代码 (-O2)。不幸的是,我的想法已经不多了。这种行为的原因可能是什么?
【问题讨论】:
-
二次幂的大峰值和二次幂的小倍数可能是由于:stackoverflow.com/questions/12264970/…
-
你在为 AVX 编译吗?这将使 32 字节对齐变得重要。
-
我使用的是 GCC 的默认值,从程序集转储看来 AVX 没有被使用。不过谢谢你的链接,我会读的。
-
谢谢,这似乎是问题所在。我的计算机上的 L2 大小为 32kB,缓存线为 64B。应该有 32kB/64B=512 缓存行可用。那么在我们遍历列之后,整个缓存不应该没用吗?应该需要记住1536行。当然,除非计算机使用比 LRU 更智能的东西......
-
啊,没关系,我查了 L1。 L2 是 256kB,所以问题应该非常清楚(256kB 大约是容纳单列的五倍)。再次感谢您的帮助。唯一需要解释的是为什么
N%8会产生如此大的影响。
标签: c++ performance caching matrix