【问题标题】:Branchless version of swapping x with y if x > y?如果 x > y,用 y 交换 x 的无分支版本?
【发布时间】:2019-03-11 16:29:52
【问题描述】:

假设xy 是有符号整数,是否有一些超级有效的实现技巧:

if (x < y) {
    std::swap(x, y);
}

我可以立即想到使用c = x &lt; y 的解决方案,然后将x 分配给c * x + (1 - c) * y 等,但这种方法会发出乘法指令,我想避免这种方法。有没有办法只用一点点摆弄来做到这一点?

编辑:只是澄清我真正关心的是试图摆脱由if 引起的分支。换句话说,我知道进行交换的 XOR 技巧,但这不是我要问的。

【问题讨论】:

  • 编译器很聪明——你为什么要担心这个?
  • 我想是否有可能摆脱分支取决于 CPU(例如,上述 gobolt 链接基于类似 Intel 的东西)
  • 分支并不总是很慢,只要比较是可预测的(并且 CPU 真的很擅长)。您的代码就像 X = x; Y = y; x = std::max(X, Y); y = std::min(X, Y); 一样,无需分支即可轻松完成,但需要 2 个 cmovcc,这可能比分支慢。根据我的经验,ICC 使用cmovcc 比 Clang 更多,而 Clang 又比 gcc 更频繁地发出这些指令。 Sample
  • 几乎所有上述替代方案都只是用代码中不太明显的分支替换了一个容易看到的分支c = x &lt; y?:std::max 都包含一个基本解析为分支(或 2)的条件。在大多数示例中,替代代码更复杂并且可能更慢。 如果有优化,编译器可能还是会打结。除非分析显示卷轴性能问题,否则请避免使代码更难理解的微优化。如果你仍然编写这样的代码,请确保性能增益足够大以证明它是合理的。
  • 您是否使用cmp/mov/cmovele/cmovl (the code clang emits) 将此代码与内联汇编进行了基准测试? Sometimes branching is faster than a conditional move (here's another reference)。如果您可以将cmpcmov 展开,它也可能会有所帮助。

标签: c++ c++11 optimization


【解决方案1】:

我不确定,这是否可以加速您的代码,但这是无分支解决方案:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv) {
  int a = atoi(argv[1]);
  int b = atoi(argv[2]);
  int c = a - b;
  c &= c >> 31; // SXT for signed int
  a -= c;
  b += c;
  printf("Result: %d %d\n", a, b);
}

【讨论】:

  • &gt;&gt; 31 调用实现定义的行为,不是吗?
  • 31 依赖于架构。你可以使用更正确但更丑陋的:(sizeof(int)*8 - 1)
  • 不仅仅是 31 或 8 (CHAR_BIT)。这也与负值有关。您可能需要-(c &lt; 0) 而不是c &gt;&gt; 31。并且整个代码仍然依赖于 2 的补码表示。
  • 感谢提示,很好。我比较了两种变体,clang 为两者生成完全相同的代码。但是,(c
  • 现代编译器不能保证有或没有分支。 :)
【解决方案2】:

如果xy 在此操作之后被写入内存,那么您可以使用写入动态内存位置而不是条件跳转。比如对a[0], a[1]进行排序:

int x = a[0];
int y = a[1];
a[x >= y] = x;
a[y > x] = y;

如果您需要立即读回这些值,那么它可能会比可预测的分支更慢,但这可能取决于处理器。

【讨论】:

  • 在此基础上,您可以通过int a[2] = {x, y}; int lt = x &lt; y; x = a[lt]; y = a[lt ^ 1]; 执行交换。虽然我不希望它比原始代码执行得更好。
【解决方案3】:

实现交换的最有效方法是识别您有名称和与名称关联的数据,并且您可以交换名称而不是交换数据。

例如,编译器可以转换:

if (x < y) {
    std::swap(x, y);
}
do_something(x, y);
return x;

..进入这个:

if (x < y) {
    // Names of "x" and "y" swapped in subsequent code
    do_something(y, x);
    return y;
} else {
    do_something(x, y);
    return x;
}

当然,交换名称而不交换数据通常是免费的(出于性能考虑),因为您实际上并没有交换任何东西。

现代 CPU 也能做到这一点。

具体来说; CPU 有寄存器,寄存器是与数据相关的名称。对于像xchg eax,ebx(在 80x86 上)这样的指令,CPU 只会交换寄存器名称而不会移动数据。这意味着 CPU 可以在任一寄存器中的数据未知时进行交换(例如,因为它仍在被前一条指令计算或获取)。

换句话说;实现std::swap(x, y); 的最快方法是确保为 CPU 生成正确的指令(例如,在 80x86 上给 CPU 一个xchg eax,ebx,它没有分支并且不必等到值已知)。

【讨论】:

    【解决方案4】:

    根据其他人的建议,您可以尝试根据std::min()std::max() 重写您的代码。

    但不能保证。该语言只是缺乏一种方法来表达您想要从编译器中得到什么。

    关于我可能提供的唯一其他非 C++ 解决方案是内联汇编,您可以在其中精确地编写您想要的指令。但是,使用内联汇编会影响编译器对其周围代码的处理,并且可能会产生负面影响(例如,寄存器使用效率低下、寄存器溢出等),从而抵消或抵消任何预期收益。

    【讨论】:

    • 谁说std::minstd::max 不分支? gcc 库基本上就像if(b&gt;a) return b; else return a; 一样实现了这一点,参见github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/…
    • @Handy999 我想你不明白。不能保证会有或不会有分支。这完全取决于编译器,并且没有语言结构可以明确地使编译器倾斜以发出一个或禁止发出一个。
    • 这对我来说是完全清楚的,我完全同意您无法直接控制创建的代码!尽管如此,我认为 min 和 max 是非分支结构的想法是完全荒谬的,因为它们正是这样做的函数。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多