为什么这些矩阵乘法的性能如此不同?

时间:2010-10-27 00:38:58

标签: java performance matrix-multiplication

我在Java中编写了两个矩阵类,只是为了比较矩阵乘法的性能。一个类(Mat1)存储double[][] A成员,其中矩阵的行iA[i]。另一个类(Mat2)存储AT,其中TA的转置。

假设我们有一个方阵M,我们想要M.mult(M)的乘积。请致电产品P

当M是Mat1实例时,使用的算法很简单:

P[i][j] += M.A[i][k] * M.A[k][j]
    for k in range(0, M.A.length)

在M是我使用的Mat2的情况下:

P[i][j] += M.A[i][k] * M.T[j][k]

,因为T[j][k]==A[k][j]是相同的算法。在1000x1000矩阵上,第二个算法在我的机器上花费大约1.2秒,而第一个算法花费至少25秒。我期待第二个更快,但不是这么多。问题是,为什么这么快?

我唯一的猜测是第二个更好地利用了CPU缓存,因为数据以大于1个字的块的形式被拉入缓存,而第二个算法通过仅遍历行来获益,而第一个算法忽略了数据通过立即到达下面的行(在内存中大约1000个字,因为数组按行主要顺序存储)拉入缓存中,没有任何数据被缓存。

我问了一个人,他认为这是因为更友好的内存访问模式(即第二个版本会导致更少的TLB软故障)。我根本没有想到这一点,但我可以看看它如何导致更少的TLB故障。

那么,这是什么?还是有其他原因导致性能差异?

4 个答案:

答案 0 :(得分:5)

这是因为您的数据的位置。

在RAM中,矩阵虽然从您的角度来看是二维的,但它当然存储为一个连续的字节数组。与1D数组的唯一区别在于,通过插入您使用的两个索引来计算偏移量。

这意味着如果您访问位置x,y处的元素,它将计算x*row_length + y,这将是用于引用指定位置元素的偏移量。

大小的矩阵不会只存储在内存页面中(这就是操作系统管理RAM的方式,将其拆分为块)所以如果你试试它必须在CPU缓存中加载正确的页面访问尚未存在的元素。

只要连续进行乘法运算就不会产生任何问题,因为你主要使用页面的所有系数然后切换到下一个系数,但如果你反转索引,那么每个元素都可能是包含在一个不同的内存页面中,所以每当它需要向RAM请求一个不同的页面时,这几乎就是你所做的每一次乘法,这就是差异如此简洁的原因。

(我宁愿简化整个解释,只是为了给你解决这个问题的基本想法)

在任何情况下,我都不认为这是由JVM本身引起的。它可能与您的操作系统如何管理Java进程的内存有关。

答案 1 :(得分:0)

缓存和TLB假设都是合理的,但我希望看到基准测试的完整代码...而不仅仅是伪代码片段。

另一种可能性是性能差异是由于您的应用程序使用转置版本的数据阵列使用了50%以上的内存。如果JVM的堆大小很小,则可能导致GC过于频繁地运行。这很可能是使用默认堆大小的结果。 (三个1000 x 1000 x 8个字节是~24Mb)

尝试将初始和最大堆大小设置为(比方说)当前最大大小的两倍。如果这没有区别,那么这不是一个简单的堆大小问题。

答案 2 :(得分:0)

很容易猜到问题可能是地方,也许是,但这仍然是猜测。

没有必要猜测。两种技术可能会给你答案 - 单步和随机暂停。

如果你单步执行慢速代码,你可能会发现它正在做很多你梦寐以求的事情。比如,你问?试一试,找出答案。在机器语言层面,你应该看到它做什么,就是有效地逐步完成内部循环而没有浪费。

如果它实际上是在没有浪费运动的情况下踩过内环,那么随机暂停将为您提供信息。由于速度较慢的速度比快速速度长20倍,这意味着95%的时间它正在做一些它不必要的事情。所以看看它是什么。每次你暂停它,你有95%的机会看到它是什么,为什么。

如果在慢速情况下,它正在执行的指令看起来和快速情况一样有效,那么缓存局部性是对它缓慢的合理猜测。我敢肯定,一旦你消除了可能发生的任何其他愚蠢行为,缓存局部性占主导地位。

答案 3 :(得分:0)

您可以尝试比较JDK6和OpenJDK7之间的性能,给定set of results ...