SSE2 双倍乘法比标准乘法慢

Posted

技术标签:

【中文标题】SSE2 双倍乘法比标准乘法慢【英文标题】:SSE2 double multiplication slower than with standard multiplication 【发布时间】:2011-07-03 19:25:42 【问题描述】:

我想知道为什么以下带有 SSE2 指令的代码执行乘法比标准 C++ 实现慢。 代码如下:

        m_win = (double*)_aligned_malloc(size*sizeof(double), 16);
        __m128d* pData = (__m128d*)input().data;
        __m128d* pWin = (__m128d*)m_win;
        __m128d* pOut = (__m128d*)m_output.data;
        __m128d tmp;
        int i=0;
        for(; i<m_size/2;i++)
            pOut[i] = _mm_mul_pd(pData[i], pWin[i]);

m_output.datainput().data 的内存已使用 _aligned_malloc 分配。

但是对于 2^25 数组,执行此代码的时间与此代码的时间相同(350 毫秒):

for(int i=0;i<m_size;i++)
    m_output.data[i] = input().data[i] * m_win[i];

这怎么可能?理论上应该只需要 50% 的时间,对吧?还是从 SIMD 寄存器到 m_output.data 数组的内存传输开销如此之大?

如果我从第一个 sn-p 替换该行

pOut[i] = _mm_mul_pd(pData[i], pWin[i]);

tmp = _mm_mul_pd(pData[i], pWin[i]);

__m128d tmp; 然后代码执行得非常快,比我的计时器功能的分辨率还小。 是不是因为所有内容都只存储在寄存器中而不是内存中?

更令人惊讶的是,如果我在调试模式下编译,SSE 代码只需要 93ms,而标准乘法需要 309ms

调试:93ms (SSE2) / 309ms(标准乘法) RELEASE:350ms (SSE2) / 350(标准乘法)

这是怎么回事???

我在发布模式下使用带有 QtCreator 2.2.1 的 MSVC2008。 这是我的 RELEASE 编译器开关:

cl -c -nologo -Zm200 -Zc:wchar_t- -O2 -MD -GR -EHsc -W3 -w34100 -w34189

这些是用于调试的:

cl -c -nologo -Zm200 -Zc:wchar_t- -Zi -MDd -GR -EHsc -W3 -w34100 -w34189

编辑 关于 RELEASE 与 DEBUG 问题: 我只是想指出,我分析了代码,而 SSE 代码在发布模式下实际上速度较慢! 这只是以某种方式证实了 VS2008 无法正确处理优化器的内在函数的假设。 英特尔 VTune 在 DEBUG 中为 SSE 循环提供了 289 毫秒,在 RELEASE 模式下为我提供了 504 毫秒。 哇……哇哇……

【问题讨论】:

您在调试模式下得到正确的结果吗?随着存储到tmp,编译器注意到结果没有被使用,甚至根本没有循环。此外,三个阵列中的每一个都是 256 MB,因此 RAM 带宽应该是决定因素。另请注意,Visual C++ 能够将普通算术表达式转换为 SSE 指令,尽管您似乎没有激活该选项。 您没有发布您测试的确切代码和数据,也没有指定您使用的核心,因此无法获得可比较的结果。在 Debug 构建中,for(;;) 循环的时间增加了一倍,这是意料之中的。在 Release 版本中,我看到 for(;;) 循环快了近三倍。看一下生成的机器代码就很清楚为什么:循环展开了,每次通过 8 次乘法。优化器无法对内在函数进行相同的优化。永远不要低估优化器的力量。 /arch:SSE2 是打开它的开关吗?但似乎 RAM 带宽确实可能是瓶颈,这意味着我无法使用 SSE 使其更快。 @Hans Passant:嗯,有趣的提示。我期待优化器也能处理 SSE。 一定要对内存带宽限制的言论加一点盐,你的测试证明它不是。如果是这样,展开循环永远不会有好处。这些天很难获得指令时序,但我确实在英特尔手册中看到 mulpd 具有 9 个周期的延迟和 9 个周期的 Atom 内核吞吐量。这有点多。 【参考方案1】:

首先,VS 2008 对于 intrisincs 来说是一个糟糕的选择,因为它往往会添加比必要更多的寄存器移动,并且通常不能很好地优化(例如,当 SSE 指令执行时,它在循环归纳变量分析方面存在问题存在。)

所以,我的猜测是编译器生成 mulss 指令,CPU 可以轻松地重新排序和并行执行(迭代之间没有依赖关系),而 intrisincs 会导致大量寄存器移动/复杂的 SSE 代码——它甚至可能破坏现代 CPU 上的跟踪缓存。 VS2008 因在寄存器中进行所有计算而臭名昭著,我猜会有一些 CPU 无法跳过的危险(如 xor reg、move mem->reg、xor、mov mem->reg、mul、mov mem->reg是一个依赖链,而标量代码可能是 move mem->reg, mul with mem operand, mov。)您绝对应该查看生成的程序集或尝试 VS 2010,它对 intrinsincs much 有更好的支持.

最后,也是最重要的一点:您的代码完全不受计算限制,再多的 SSE 也不会显着加快速度。在每次迭代中,您读取四个双精度值并写入两个,这意味着 FLOP 不是您的问题。在这种情况下,您将受到缓存/内存子系统的支配,这可能解释了您看到的差异。调试乘法不应该比发布快;如果你发现它比你应该做更多的运行速度更快,并检查发生了什么(如果你的 CPU 支持涡轮模式,请小心,这会增加 20% 的变化。)清空缓存的上下文切换可能就足够了这种情况。

因此,总的来说,您所做的测试几乎毫无意义,只是表明对于内存受限的情况,是否使用 SSE 没有区别。如果实际上存在计算密集且并行的代码,则应该使用 SSE,即使这样,我也会花费大量时间使用分析器来确定要优化的确切位置。简单的点积不适合看到 SSE 的任何性能改进。

【讨论】:

双注:(1) 这不是点积,它会使用乘加(累加)指令,并且写入带宽要低得多。 (2) 即使代码不受计算限制,使用更高效的指令应该留出更多时间来运行其他受计算限制的线程,或者如果没有其他线程在运行,则使用更少的功率。 你在这里得到了一些好处,所以+1。但是,由于 VC2010 不是一个选项,我看到计算(只有 1 次乘法)并不是真正的 CPU 密集型,但每条指令的 2 次乘法不应该抵消 SSE 代码生成的开销吗?或者用多个htreads简单地计算它会更好吗?数组很大,函数调用会很频繁。 好吧,我真的认为是内存带宽。然而,没有什么能解释为什么我的调试运行得比发布快。但是感谢您帖子中的其他有见地的 cmets 以及所有其他人!【参考方案2】:

几点:

正如已经指出的那样,MSVC 为 SSE 生成了非常糟糕的代码 您的代码几乎可以肯定是内存带宽有限,因为您在加载和存储之间只执行一项操作 大多数现代 x86 CPU 有两个浮点 ALU,因此即使您不受带宽限制,使用 SSE 进行双精度浮点数学运算也可能没什么收获

【讨论】:

以上是关于SSE2 双倍乘法比标准乘法慢的主要内容,如果未能解决你的问题,请参考以下文章

sse2浮点乘法

偏最小二乘法的岭回归分析

你如何在 SSE2 上进行带符号的 32 位扩展乘法?

SPSS中异方差如何用WLS加权最小二乘消除

[AHOI2009]维护序列

是否可以在没有 SSE4 的情况下在 VC++ 中向量化乘法?