【问题标题】:Matrix multiplication speed issues矩阵乘法速度问题
【发布时间】: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


【解决方案1】:

对您的示例来说最重要的是 - CPU 缓存行大小。对于 CPU,它通常是 64 字节。即使您的程序读取或写入 1 个字节,CPU 也会读取/写入所有行(64 个字节)。这就是为什么,如果你的程序命中缓存行,你的性能就很好。如果它错过了,读取/写入内存会有额外的开销。 L3 缓存的大小并不那么重要。

为您的代码

// all your stack variables are good. Compiler will optimize them well. 
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] *  // here you are good, you read memory sequentially 
                  b[k][j]; // here, you are not good, every read comes from different cache line
        }
        c[i][j] = sum; // here doesn't matter, it is rare operation
    }
}

类似于您的情况is here。该演示文稿很好地解释了如何优化此类代码以及它为什么以这种方式工作。我希望你能找到你需要的一切。

【讨论】:

    猜你喜欢
    • 2012-09-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-05-07
    • 1970-01-01
    相关资源
    最近更新 更多