我需要每秒运行 240000 次矩阵向量乘法。矩阵是 5x5 并且始终相同,而向量在每次迭代时都会发生变化。数据类型是float
。我正在考虑使用一些 SSE(或类似)指令。
我担心算术运算的数量与所涉及的内存操作的数量相比太少了。你认为我能得到一些切实的(例如> 20%)改进吗?
我需要英特尔编译器吗?
你能指出一些参考资料吗?
我需要每秒运行 240000 次矩阵向量乘法。矩阵是 5x5 并且始终相同,而向量在每次迭代时都会发生变化。数据类型是float
。我正在考虑使用一些 SSE(或类似)指令。
我担心算术运算的数量与所涉及的内存操作的数量相比太少了。你认为我能得到一些切实的(例如> 20%)改进吗?
我需要英特尔编译器吗?
你能指出一些参考资料吗?
原则上,SSE 的加速可以是 4 倍(AVX 是 8 倍)。让我解释。
让我们称您的固定 5x5 矩阵M。将 5D 向量的分量定义为 (x,y,z,w,t)。现在从前四个向量 形成一个 5x4 矩阵U。
U =
xxxx
yyyy
zzzz
wwww
tttt
接下来,做矩阵乘积MU = V。矩阵V包含M和前四个向量的乘积。唯一的问题是,对于 SSE,我们需要读取U的行,但在内存中U存储为xyzwtxyzwtxyzwtxyzwt,因此我们必须将其转置为xxxxyyyyzzzzwwwwtttt。这可以通过 SSE 中的洗牌/混合来完成。一旦我们有了这种格式,矩阵乘积就非常有效。
而不是使用标量代码进行 O(5x5x4) 操作,它只需要 O(5x5) 操作,即 4 倍加速。使用 AVX,矩阵U将是 5x8,因此它不需要 O(5x5x8) 操作,它只需要 O(5x5),即 8 倍加速。
然而,矩阵V将采用xxxxyyyyzzzzwwwwtttt格式,因此根据应用程序,它可能必须转换为xyzwtxyzwtxyzwtxyzwt格式。
对接下来的四个向量(AVX 为 8 个)重复此操作,依此类推,直到完成。
如果您可以控制向量,例如,如果您的应用程序动态生成向量,那么您可以以xxxxyyyyzzzzwwwwtttt格式生成它们并避免数组的转置。在这种情况下,您应该使用 SSE 获得 4 倍的加速,使用 AVX 获得 8 倍的加速。如果您将其与线程(例如 OpenMP)结合使用,您的加速应该接近 16 倍(假设四个物理内核)与 SSE。我认为这是你可以用 SSE 做的最好的事情。
编辑:由于指令级并行性(ILP),您可以获得另一个 2 倍的加速,因此 SSE 的加速可以通过四个内核(64x AVX)提高 32 倍,并且由于 FMA3,Haswell 的加速可以再次提高 2 倍。
我建议使用英特尔 IPP 并抽象出对技术的依赖
如果您使用 GCC,请注意 -O3 选项将启用自动矢量化,这将在许多情况下自动生成 SSE 或 AVX 指令。一般来说,如果你只是把它写成一个简单的 for 循环,GCC 会将它向量化。有关更多信息,请参阅http://gcc.gnu.org/projects/tree-ssa/vectorization.html。
这应该很容易,尤其是当您使用 Core 2 或更高版本时:您需要 5* _mm_dp_ps
、 one _mm_mul_ps
、 two _mm_add_ps
、 one 普通乘法,以及一些随机播放、加载和存储(如果矩阵是固定的,您可以保留大部分在 SSE 寄存器中,如果您不需要它们用于其他任何事情)。
至于内存带宽:我们说的是 2.4 MB 的向量,而内存带宽是每秒个位数千兆字节。
对向量有什么了解?由于矩阵是固定的,并且如果向量可以采用的值数量有限,那么我建议您预先计算计算并使用表格查找来访问它们。
用内存换周期的经典优化技术...
我建议查看优化的 BLAS 库,例如 Intel MKL 或 AMD ACML。根据您的描述,我假设您将在SGEMV
2 级矩阵向量例程之后进行y = A*x
样式操作。
如果你真的想自己实现一些东西,使用(可用的)SSE..SSE4
和AVX
指令集可以在某些情况下提供显着的性能改进,尽管这正是一个好的 BLAS 库要做的事情。您还需要仔细考虑缓存友好的数据访问模式。
我不知道这是否适用于您的情况,但是您可以一次对向量的“块”进行操作吗?因此,y = A*x
您可以对[y1 y2 ... yn] = A * [x1 x2 ... xn]
. 如果是这样,这意味着您可以使用优化的矩阵矩阵例程,例如SGEMM
. 由于数据访问模式,这可能比重复调用更有效SGEMV
。如果是我,我会尝试走这条路……
希望这可以帮助。
如果您事先知道向量(例如,一次完成所有 240k),那么通过并行化循环比通过 SSE 获得更好的加速。如果您已经迈出了这一步,或者您不是一下子了解所有这些,那么 SSE 可能会带来很大的好处。
如果内存是连续的,那么不要太担心内存操作。如果你有一个链表或其他东西,那么你就有麻烦了,但它应该能够跟上而没有太多问题。
5x5 是一个有趣的大小,但您可以在一条 SSE 指令中至少执行 4 次翻转,并尝试减少算术开销。您不需要英特尔编译器,但它可能会更好,我听说过关于它如何使用算术代码更好的传说。Visual Studio 具有处理 SSE2 的内在函数,我认为最多 SSE4 取决于您的需要。当然,你必须自己滚动它。抓住图书馆可能是这里的明智之举。