双布尔乘法有多快,它能被矢量化吗
how fast is double*bool multiplication, can it be vectorized?
我将常数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;});
}
似乎您正试图使用vector<bool>
的掩码来归零vector<double>
的部分。
目前的情况是,它不可向量化。此外,vector<bool>
模板专用化将阻碍编译器进行任何类型的自动向量化。
所以你基本上有两个选择:
简单的方法是将vector<bool>
转换为相应的零和一的vector<double>
。然后,问题简化为同一数据类型的简单向量到向量相乘,这是完全可向量化的。(甚至可自动矢量化)
更难的方法(可能更快)是使用_mm_and_pd
或_mm_blendv_pd()
内部函数/指令进行一些破解。但这需要做更多的工作,因为您必须手动向量化代码。
我建议你选择简单的方法。除非您真的需要,否则无需深入手动矢量化。
我试过这两种方法,你的函数和你的问题一样,还有这个:
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的向量更改为两倍,以查看每个选项之间的差异。
最后,我提出了一个完全不同的算法,因为我认为在这种情况下可以使用更好的算法。
乘法
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个指令";。它会因重复而变化。三元算子
在这种情况下,我们选择布尔值为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
用于循环,就像上面的代码一样。两个双矢量
另一方面,当我们使用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的答案是正确的。不过,我并没有试着看看每一套指令的执行速度有多快。这并不是并行的。如果您想要完全并行化,那么您肯定必须在汇编中编写它,或者至少使用内部函数。
装配
使用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)
加载一个值到%zmm0
,在对应掩码位为0
的任何位置屏蔽双精度(将它们设置为全零)。您还需要一条指令将
%zmm0
保存回内存,两条指令增加指针,以及一个计数器和一个分支。因此,在C++的最佳情况下,8个指令而不是9*8=72。无乘法,单次转换非常快(kmovb
)。唯一的限制是:数组的大小必须是8的倍数。你也可以使用内在的,类似这样的东西:
#include <immintrin.h> __mmask8 mask = 0xA5; __m512d a, b; __m512d res = _mm512_mask_blend_pd( mask, a, b );
你必须检查一下文件。没有使用a或b中的一个。
注意,这是一个";整数";指示它可以使用doubles,因为我们要么按原样加载64位,要么将其设置为全零,这与
(double)0
相同。了解算法
仔细想想你的问题,我还注意到你试图做的是在数组中保存一些零。您可以采取不同的做法,避免一个完整的加载/多重/保存周期。
最接近支持该功能的C++算法是std::replace_if。问题是,测试针对的是与被替换的输入相同的数组值。所以在你的情况下,这没有帮助。
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; } } }
这里的一个缺点是CCD_ 27中的CCD_。我觉得它不干净。但就算法而言,它使它更快。如果你只屏蔽了很少的两次,它将比
transform()
更快地完成最后一次读取/修改/写入循环。
- 为什么 openmp 的并行不适用于矢量化色彩空间转换?
- GCC 4.8.2 自动矢量化由于 cout 而失败
- 为什么浮点数的矢量化比双精度更有效?
- GCC、CLANG 和 MSVC 的可视化C++自动矢量化要求
- 如何使 msvc 矢量化浮点添加?
- 我可以期望某些 STL 函数实现是可自动矢量化的吗?
- 当在循环中使用时,std::shared_ptr 对该循环的矢量化有任何影响吗?
- 使用 true 初始化布尔数组时出现问题
- 矢量化图像处理
- MSVC 2017 是否支持具有自动矢量化的 AVX 512
- 矢量化对称矩阵
- 如何在块复制期间矢量化范围检查
- 是否可以使用G 或Clang -OpenMP获得矢量化报告
- 错误的矢量化代码会影响可伸缩性吗?
- C 矢量化双回路
- 如何将现有的矢量化函数与Intel编译器自动化的现有标量函数相关
- 在C++中初始化布尔向量的标准方法是什么
- 用于自动矢量化的展开指针增量循环
- 这是矢量化的良好实践吗
- 对于使用 C 样式指针矢量化的循环,但不使用迭代器