我有一个通用代码,我试图转移到SSE以加快速度,因为它被调用了很多。有问题的代码基本上是这样的:
for (int i = 1; i < mysize; ++i)
{
buf[i] = myMin(buf[i], buf[i - 1] + offset);
}
其中myMin是你的简单最小函数(a&lt; b)? a:b(我看过拆卸,这里有跳跃)
我的SSE代码(我已经经历了几次迭代以加快速度)现在处于这种形式:
float tmpf = *(tmp - 1);
__m128 off = _mm_set_ss(offset);
for (int l = 0; l < mysize; l += 4)
{
__m128 post = _mm_load_ps(tmp);
__m128 pre = _mm_move_ss(post, _mm_set_ss(tmpf));
pre = _mm_shuffle_ps(pre, pre, _MM_SHUFFLE(0, 3, 2, 1));
pre = _mm_add_ss(pre, off);
post = _mm_min_ss(post, pre);
// reversed
pre = _mm_shuffle_ps(post, post, _MM_SHUFFLE(2, 1, 0, 3));
post = _mm_add_ss(post, off );
pre = _mm_min_ss(pre, post);
post = _mm_shuffle_ps(pre, pre, _MM_SHUFFLE(2, 1, 0, 3));
pre = _mm_add_ss(pre, off);
post = _mm_min_ss(post, pre);
// reversed
pre = _mm_shuffle_ps(post, post, _MM_SHUFFLE(2, 1, 0, 3));
post = _mm_add_ss(post, off);
pre = _mm_min_ss(pre, post);
post = _mm_shuffle_ps(pre, pre, _MM_SHUFFLE(2, 1, 0, 3));
_mm_store_ps(tmp, post);
tmpf = tmp[3];
tmp += 4;
}
忽略任何我处理得很好的边缘情况,并且由于buf / tmp的大小,这些情况可以忽略不计,有人可以解释为什么SSE版本慢了2倍吗? VTune将其归因于L1未命中,但正如我所看到的,它应该减少4倍的L1行程并且没有分支/跳跃,因此应该更快,但事实并非如此。我在这里误会了什么?
由于
编辑: 所以我确实在一个单独的测试用例中找到了其他东西。我认为这不重要,但它确实如此。所以上面的mysize实际上并不是那么大(大约30-50),但是有很多这些并且它们都是连续完成的。在这种情况下,三元表达式比SSE更快。但是,如果它与mysize相反,并且只有30-50次迭代,则SSE版本更快。知道为什么吗?我认为两者的记忆互动都是一样的,包括先发制人的预取等......
答案 0 :(得分:1)
如果此代码对性能至关重要,则必须查看所获得的数据。这是杀死你的串行依赖,你需要摆脱它。
一个非常小的值buf [i]会影响以下很多值。例如,如果offset = 1,则buf [0] = 0,并且所有其他值> 100万,那一个价值将影响下一百万。另一方面,这种事情可能很少发生。
如果很少见,你可以检查完全向量化是否buf [i]&gt; buf [i] + offset,如果是,则替换它,并跟踪进行更改的位置,而不考虑buf [i]值可能向上涓流。然后检查更改的位置,并重新检查它们。
在极端情况下,假设buf [i]始终在0和1之间,并且偏移&gt; 0.5,你知道buf [i]根本不能影响buf [i + 2],所以你只需要忽略串行依赖并完成所有并行操作,完全矢量化。
另一方面,如果你的缓冲区中有一些影响大量连续值的微小值,那么你从第一个值buf [0]开始并完全向量化检查是否buf [i]&lt; buf [0] + i * offset,替换值,直到检查失败。
你说“价值观可以是任何东西”。如果是这种情况,例如,如果buf [i]随机选择在0到1,000,000之间的任何地方,并且偏移量不是很大,那么你将有元素buf [i]强制许多后续元素为buf [i] +(k - i)*偏移量。例如,如果offset = 1,并且您发现buf [i]大约为10,000,那么它将平均强制约100个值等于buf [i] +(k-i)* offset。
答案 1 :(得分:0)
这是您可以尝试的无分支解决方案
for (int i = 1; i < mysize; i++) {
float a = buf[i];
float b = buf[i-1] + offset;
buf[i] = b + (a<b)*(a-b);
}
这是集会:
.L6:
addss xmm0, xmm4
movss xmm1, DWORD PTR [rax]
movaps xmm2, xmm1
add rax, 4
movaps xmm3, xmm6
cmpltss xmm2, xmm0
subss xmm1, xmm0
andps xmm3, xmm2
andnps xmm2, xmm5
orps xmm2, xmm3
mulss xmm1, xmm2
addss xmm0, xmm1
movss DWORD PTR [rax-4], xmm0
cmp rax, rdx
jne .L6
但是带分支的版本可能已经更好了
for (int i = 1; i < mysize; i++) {
float a = buf[i];
float b = buf[i-1] + offset;
buf[i] = a<b ? a : b;
}
这是程序集
.L15:
addss xmm0, xmm2
movss xmm1, DWORD PTR [rax]
add rax, 4
minss xmm1, xmm0
movss DWORD PTR [rax-4], xmm1
cmp rax, rdx
movaps xmm0, xmm1
jne .L15
使用minss
生成无分支的代码(cmp rax, rdx
适用于循环迭代器)。
最后,这里是可以与MSVC一起使用的代码,它生成与GCC无组件相同的程序集
__m128 offset4 = _mm_set1_ps(offset);
for (int i = 1; i < mysize; i++) {
__m128 a = _mm_load_ss(&buf[i]);
__m128 b = _mm_load_ss(&buf[i-1]);
b = _mm_add_ss(b, offset4);
a = _mm_min_ss(a,b);
_mm_store_ss(&buf[i], a);
}
您可以尝试使用分支
的另一种形式__m128 offset4 = _mm_set1_ps(offset);
for (int i = 1; i < mysize; i++) {
__m128 a = _mm_load_ss(&buf[i]);
__m128 b = _mm_load_ss(&buf[i-1]);
b = _mm_add_ss(b, offset4);
if(_mm_comige_ss(b,a))
_mm_store_ss(&buf[i], b);
}