未知的SSE瓶颈

时间:2014-09-16 00:17:51

标签: optimization floating-point x86 sse simd

我有一个通用代码,我试图转移到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版本更快。知道为什么吗?我认为两者的记忆互动都是一样的,包括先发制人的预取等......

2 个答案:

答案 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);
}
相关问题