【问题标题】:Unknown SSE bottleneck未知的 SSE 瓶颈
【发布时间】:2014-09-16 00:17:50
【问题描述】:

我有一个通用代码,我正试图将其移至 SSE 以加快它的速度,因为它经常被调用。有问题的代码基本上是这样的:

for (int i = 1; i < mysize; ++i)
{
    buf[i] = myMin(buf[i], buf[i - 1] + offset);
}

myMin 是你的简单 min 函数 (a

我的 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 版本更快。知道为什么吗?我认为两者的内存交互是相同的,包括先发制人的预取等......

【问题讨论】:

  • SSE 版本实际上是否比原始版本更平行?
  • 串行依赖是在这里杀死你的原因——它使循环不适合 SIMD 矢量化,因此你在 SIMD 循环中做了很多工作。专注于优化标量循环可能会更有成效:确保您使用无分支 min 没有任何不必要的 floatdouble 转换,并且可能还手动展开循环(当然要小心依赖关系)。跨度>
  • 分析器错误。该循环不可矢量化 - 除非您尝试类似并行前缀 min.
  • 如果您知道buf[i] 在大多数情况下都小于buf[i-1],那么您可能可以使用_mm_movemask_epi8 来加快速度。
  • 感谢您的回复。这些值可以是任何东西。关于无分支分钟,我也没有成功。我在 x64 上执行此操作,上面的简单三元表达式无法编译为无分支最小值,编译代码中有跳转(VS2010)。有什么建议如何强制它在 x64 上无分支?

标签: optimization floating-point x86 sse simd


【解决方案1】:

如果此代码对性能至关重要,则必须查看获得的数据。杀死你的是串行依赖,你需要摆脱它。

一个非常小的值 buf [i] 会影响很多以下值。例如,如果 offset = 1,buf [0] = 0,并且所有其他值 > 100 万,则该值将影响下一个 100 万。另一方面,这种事情可能很少发生。

如果很少见,他们检查是否完全矢量化了 buf [i] > buf [i] + 偏移量,如果是则替换它,并跟踪更改的位置,而不考虑 buf [i] 值可能会涓涓细流向上。然后检查更改的位置,然后重新检查它们。

在极端情况下,假设 buf [i] 始终介于 0 和 1 之间,并且 offset > 0.5,你知道 buf [i] 根本无法影响 buf [i + 2],所以你只需忽略串行依赖和并行执行所有操作,完全矢量化。

另一方面,如果缓冲区中有一些影响大量连续值的微小值,则从第一个值 buf [0] 开始并完全矢量化检查是否 buf [i]

你说“值可以是任何东西”。如果是这种情况,例如,如果 buf [i] 是在 0 到 1,000,000 之间的任意位置随机选择的,并且偏移量不是很大,那么您将拥有 buf [i] 元素,它会强制许多以下元素成为 buf [i] + (k - i) * 偏移量。例如,如果 offset = 1,并且您发现 buf [i] 约为 10,000,那么它将强制平均约 100 个值等于 buf [i] + (k - i) * offset。

【讨论】:

  • 您的答案可以用我的评论来概括“如果您知道 buf[i] 大多数时候小于 buf[i-1] 那么您可能可以使用 _mm_movemask_epi8 来加快速度”并且 OP 回答“值可以是任何东西。”我推断这意味着他不能对分布的统计数据做出任何假设。
  • 这个想法是完全忽略依赖关系,所以你得到完全矢量化的代码 - 然后修复你出错的地方。无法对值的分布做出任何假设是一种逃避。当前代码基本上是标量 并且 有依赖关系。矢量化使它可能快 10 倍,这需要查看数据。
  • 然后发布一些代码来展示如何做到这一点。我怀疑它对随机数据有帮助。事实上,我认为假设没有依赖性然后纠正随机数据可能会更糟。我想一种智能方法可以监控统计数据,如果它发现数据不是很随机,可以假设数据不是随机的。在这种情况下,我可以看到一个好处,但这是工作。
【解决方案2】:

这是一个你可以尝试的无分支解决方案

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

这使用minsscmp 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);
}

【讨论】:

  • 虽然这是无分支的,但它使用的指令比需要的要多。理想情况下,对于 x86,您需要利用 FCMOV
  • @PaulR,我明白你的意思。我更新了我的答案。带有分支的版本可能已经足够好(并且更好)。它实际上没有分支,因为它使用minss。如果我在 32 位模式下编译,我会得到 fcmov,但我认为这不是一个好主意。最好使用minss
  • 我认为主要问题是 OP 正在使用 Visual Studio,这会生成分支代码。 gcc 等人已经足够聪明,无需任何额外帮助即可生成无分支代码。
  • @PaulR,是的,GCC 在这里让我很聪明。虽然我会弄清楚,因为我会分析我的功能并发现它更糟。
  • @PaulR,我在答案的末尾添加了代码,它使用内部函数来做与 GCC 相同的事情,可以与 MSVC 一起使用。
猜你喜欢
  • 2013-10-21
  • 2011-01-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-03-29
  • 1970-01-01
相关资源
最近更新 更多