double * bool乘法有多快,它可以被矢量化吗?

时间:2012-02-01 09:17:21

标签: c++ vectorization

我在不同vector<bool>上多次乘以常量vector<double>。我想知道它有多快,将它首先转换为vector<double>会不会更快,以便可以使用sse?

    void applyMask(std::vector<double>& frame, const std::vector<bool>& mask)
    {
        std::transform(frame.begin(), frame.end(), mask.begin(), frame.begin(), [](const double& x, const bool& m)->double{ return x*m;});
    }

2 个答案:

答案 0 :(得分:5)

您似乎试图使用vector<double>的掩码将vector<bool>的部分归零。

目前看来,它不可矢量化。此外,vector<bool>模板专门化将阻碍编译器进行任何类型的自动向量化。

所以你基本上有两个选择:

简单方法确实将vector<bool>转换为相应的0和1的vector<double>。然后问题简化为简单的相同数据类型的矢量到矢量乘法,这是完全可矢量化的。 (甚至可自动矢量化)

更难的方式(可能更快),是使用_mm_and_pd_mm_blendv_pd()内在函数/指令来玩一些黑客攻击。但是,由于必须手动对代码进行矢量化,这需要更多的工作。


我建议你顺便一提。除非确实需要,否则无需深入研究手动矢量化。

答案 1 :(得分:1)

我同时尝试了这两种方法,即您在问题中的功能,以及该方法:

void applyMask(std::vector<double>& frame, const std::vector<bool>& mask)
{
    std::transform(frame.begin(), frame.end(), mask.begin(), frame.begin(), [](const double& x, const bool& m)->double{ return m?x:0.0;});
}

我还尝试将bool的向量更改为两倍,以查看每个选项之间的差异。

最后,由于我认为在这种情况下可以使用更好的算法,因此我完全提出了另一种算法。

  1. 乘法

    xmm0变量是一个SSE寄存器。但这只是用于双打而不是并行化。

     b0e:       8b 50 08                mov    0x8(%rax),%edx
     b11:       66 0f ef c0             pxor   %xmm0,%xmm0
     b15:       48 83 c6 10             add    $0x10,%rsi
     b19:       48 83 c0 08             add    $0x8,%rax
     b1d:       31 c9                   xor    %ecx,%ecx
     b1f:       83 e2 01                and    $0x1,%edx
     b22:       f2 0f 2a c2             cvtsi2sd %edx,%xmm0
     b26:       f2 0f 59 46 f8          mulsd  -0x8(%rsi),%xmm0
     b2b:       f2 0f 11 46 f8          movsd  %xmm0,-0x8(%rsi)
     b30:       83 c1 01                add    $0x1,%ecx
     b33:       ba 01 00 00 00          mov    $0x1,%edx
     b38:       48 d3 e2                shl    %cl,%rdx
     b3b:       48 85 10                test   %rdx,(%rax)
     b3e:       66 0f ef c0             pxor   %xmm0,%xmm0
     b42:       0f 95 c2                setne  %dl
     b45:       83 f9 3f                cmp    $0x3f,%ecx
     b48:       0f b6 d2                movzbl %dl,%edx
     b4b:       f2 0f 2a c2             cvtsi2sd %edx,%xmm0
     b4f:       48 8d 56 08             lea    0x8(%rsi),%rdx
     b53:       f2 0f 59 06             mulsd  (%rsi),%xmm0
     b57:       f2 0f 11 06             movsd  %xmm0,(%rsi)
     b5b:       0f 85 17 01 00 00       jne    c78 <main+0x298>
    

    这是大约22条指令。 jne是循环分支。由于循环已展开多次,因此重复了8次。这也是为什么我说“大约22条指令”的原因。视重复而定。

  2. 三元运算符

    在这种情况下,我们选择布尔值为true时的值。这就增加了一个分支,这意味着代码可能以不同的速度运行,具体取决于有多少个标志为真或为假。

     a83:       83 c1 01                add    $0x1,%ecx
     a86:       ba 01 00 00 00          mov    $0x1,%edx
     a8b:       48 d3 e2                shl    %cl,%rdx
     a8e:       48 85 10                test   %rdx,(%rax)
     a91:       66 0f ef c0             pxor   %xmm0,%xmm0
     a95:       74 05                   je     a9c <main+0xbc>
     a97:       f2 0f 10 45 08          movsd  0x8(%rbp),%xmm0
     a9c:       83 f9 3f                cmp    $0x3f,%ecx
     a9f:       f2 0f 11 45 08          movsd  %xmm0,0x8(%rbp)
     aa4:       48 8d 55 10             lea    0x10(%rbp),%rdx
     aa8:       0f 84 d4 01 00 00       je     c82 <main+0x2a2>
    

    也就是说,每个循环减少11条指令。第二个je用于循环,就像上面的代码一样。

  3. 两个双打向量

    另一方面,当我们使用double时,我们避免像(1)那样进行转换,并且如果一遍又一遍地使用相同的蒙版,并且如果向量很大,那么它将是一个很好的优化:

    a9d:   31 d2                   xor    %edx,%edx
    a9f:   66 41 0f 2e 44 24 28    ucomisd 0x28(%r12),%xmm0
    aa6:   0f 9a c2                setp   %dl
    aa9:   0f 45 d0                cmovne %eax,%edx
    aac:   f2 0f 59 4b 20          mulsd  0x20(%rbx),%xmm1
    ab1:   f2 0f 11 4b 20          movsd  %xmm1,0x20(%rbx)
    ab6:   66 0f ef c9             pxor   %xmm1,%xmm1
    aba:   f2 0f 2a ca             cvtsi2sd %edx,%xmm1
    

    这是8条指令!而且我们看不到分支。但这是优化的一部分。应该至少有一个分支,所以应该有9条指令。

    因此,看来Mysticial的答案正确。不过,我没有尝试查看每组指令的执行速度。并且这不是并行的。如果要进行完全并行化,则一定要用汇编语言编写它,或者至少要使用内部函数。

  4. 程序集

    使用AVX,您可以一次加载8个带有蒙版的双打:

      8a3:   b8 a5 ff ff ff          mov    $0xffffffa5,%eax
      8a8:   c5 f9 92 c8             kmovb  %eax,%k1
      8ac:   62 f1 fd 49 28 85 50    vmovapd -0xb0(%rbp),%zmm0{%k1}
      8b3:   ff ff ff 
    

    在此示例中,我在%eax(0xA5)中放置了一个8位掩码,然后将其复制到%k1中,然后将值从-0xb0(%rbp)加载到{{1} }在相应的屏蔽位为%zmm0的任何地方屏蔽双精度字符(将它们设置为全零)。

    您将需要再一条指令来将0保存到内存中,需要两条指令来增加指针,以及一条计数器和一个分支。因此,在C ++的最佳情况下,使用8条指令而不是9 * 8 = 72。没有乘法,只有一个非常快速的转换(%zmm0)。一个约束:数组的大小必须是8的倍数。

    您还可以使用内在函数,例如:

    kmovb

    您必须检查文档。不使用a或b之一。

    请注意,这是一个“整数”指令。它可以使用双精度,因为我们要么按原样加载64位,要么将其设置为全零,这与 #include <immintrin.h> __mmask8 mask = 0xA5; __m512d a, b; __m512d res = _mm512_mask_blend_pd( mask, a, b ); 相同。

  5. 了解算法

    更多地考虑您的问题,我还注意到一个事实,您正在尝试将一些零保存在数组中。您可以采取不同的操作,并避免完整的加载/多次加载/保存周期。

    支持该功能的最接近的C ++算法为std::replace_if。问题在于测试是针对与替换输入相同的数组值进行的。因此,对于您而言,这无济于事。

    (double)0算法如下:

    std::replace_if

    在您的情况下,您需要两个输入,并且 template<class ForwardIt, class UnaryPredicate, class T> void replace_if(ForwardIt first, ForwardIt last, UnaryPredicate p, const T& new_value) { for (; first != last; ++first) { if(p(*first)) { *first = new_value; } } } 是已知的(new_value),因此不需要,尽管可以肯定会对其进行优化。现在,我们可以像这样重写您的0.0函数:

    applyMask()

    这里的一个缺点是 template<class ForwardValuesIt, class ForwardMaskIt, class T> void mask_if(ForwardValuesIt first, ForwardValuesIt last, ForwardMaskIt mf, ForwardMaskIt ml, const T& new_value = T()) { for (; first != last && mf != ml; ++first, ++ml) { if(!*ml) { *first = new_value; } } } 中的!。我认为这不干净。但是就算法而言,它使速度更快。如果您只遮掩很少的双打,那么它会比!*ml快一圈,比transform()快一个完整的读/修改/写周期。