我在不同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;});
}
答案 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的向量更改为两倍,以查看每个选项之间的差异。
最后,由于我认为在这种情况下可以使用更好的算法,因此我完全提出了另一种算法。
乘法
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)
加载到{{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 );
相同。
了解算法
更多地考虑您的问题,我还注意到一个事实,您正在尝试将一些零保存在数组中。您可以采取不同的操作,并避免完整的加载/多次加载/保存周期。
支持该功能的最接近的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()
快一个完整的读/修改/写周期。