将比较结果用作 int 真的是无分支吗?

Is using the result of comparison as int really branchless?

提问人:galinette 提问时间:12/8/2015 最后编辑:user16217248galinette 更新时间:11/13/2023 访问量:1779

问:

我在很多答案中都看到过这样的代码,作者说这是无分支的:

template <typename T> 
inline T imax (T a, T b)
{
    return (a > b) * a + (a <= b) * b;
}

但是,在当前的架构上,这真的是无分支的吗?(x86,ARM... 有没有真正的标准保证这是无分支的?

C++ C 比较 branch-prediction branchless

评论

0赞 Slava 12/8/2015
为什么需要这样的保证?
4赞 Cheers and hth. - Alf 12/8/2015
C++ 标准不保证机器代码(甚至不保证有机器代码),除了它或任何使用的东西将重现 C++ 语句语义所需的效果。这些效果是对内存内容的更改和库函数的调用。
1赞 chux - Reinstate Monica 12/8/2015
在2015年,它在许多嵌入式处理器上并非无分支。
0赞 Sebastian Mach 12/8/2015
阿尔夫说了什么。请注意,还有一个变体,其中使用了数组,根据代码的可预测性,该数组的运行速度可能会更快。但是,只有在您测量了相关性时才进行这种微优化。const T v[] = {b, a}; return v[a>b]
4赞 galinette 12/8/2015
@Slava:因为我想做大量的过早优化和过度工程。

答:

7赞 fuz 12/8/2015 #1

x86 具有一系列指令,这些指令根据标志的值将字节寄存器设置为 1 或 0。编译器通常使用它来实现这种没有分支的代码。SETcc

如果您使用“幼稚”方法

int imax(int a, int b) {
    return a > b ? a : b;
}

编译器将使用(条件移动)系列指令生成更高效的无分支代码。CMOVcc

ARM 能够有条件地执行每条指令,这使得编译器能够有效地编译 U 和朴素的实现,朴素的实现速度更快。

评论

3赞 Lundin 12/8/2015
换句话说:通过尝试手动优化代码,而不是编写尽可能可读和简单的代码,代码变得更丑陋和更慢。
0赞 SergeyA 12/8/2015
这并不奇怪。根据定义,编译器编写者通常比 C++ 程序员更了解芯片组指令。
0赞 galinette 12/8/2015
对于平台部分来说,这是一个很好的答案,但是标准中真的可以保证无分支吗?我想使用 if/else 可以作为您的“幼稚”方法,这基本上意味着我引用的这些伪优化答案与 std 函数(最小值、最大值等)相比会降低性能
0赞 fuz 12/8/2015
@galinette我不熟悉 C++ 标准,但如果它像 C 标准一样,它现在绝对可以保证某些东西是如何实现的。从标准的角度来看,甚至加法也可以通过分支来实现。
0赞 fuz 12/9/2015
@galinette 我删除了您在答案中添加的句子,因为我不熟悉C++标准,也不想对此做出任何声明。
0赞 mirabilos 8/3/2019 #2

我偶然发现了这个 SO 问题,因为我也在问我同样的问题......事实证明,并非总是如此。例如,以下代码...

const struct op {
    const char *foo;
    int bar;
    int flags;
} ops[] = {
    { "foo", 5, 16 },
    { "bar", 9, 16 },
    { "baz", 13, 0 },
    { 0, 0, 0 }
};

extern int foo(const struct op *, int);

int
bar(void *a, void *b, int c, const struct op *d)
{
    c |= (a == b) && (d->flags & 16);
    return foo(d, c) + 1;
}

...在所有优化级别中,使用 GCC 3.4.6 (i386) 和 8.3.0 (amd64, i386) 编译为分支代码。3.4.6 中的那个更手动合法,我将用以下方式进行演示:gcc -O2 -S -masm=intel x.c; less x.s

[…]
    .text
    .p2align 2,,3
    .globl   bar
    .type    bar , @function
bar:
    push     %ebp
    mov      %ebp, %esp
    push     %ebx
    push     %eax
    mov      %eax, DWORD PTR [%ebp+12]
    xor      %ecx, %ecx
    cmp      DWORD PTR [%ebp+8], %eax
    mov      %edx, DWORD PTR [%ebp+16]
    mov      %ebx, DWORD PTR [%ebp+20]
    je       .L4
.L2:
    sub      %esp, 8
    or       %edx, %ecx
    push     %edx
    push     %ebx
    call     foo
    inc      %eax
    mov      %ebx, DWORD PTR [%ebp-4]
    leave
    ret
    .p2align 2,,3
.L4:
    test     BYTE PTR [%ebx+8], 16
    je       .L2
    mov      %cl, 1
    jmp      .L2
    .size    bar , . - bar

事实证明,指针比较操作调用比较甚至子例程来插入 1。

在这里,即使不使用也有所作为。!!(a == b)

TL的;博士

检查实际编译的实际编译器输出(使用或反汇编;如果不在 x86 上,则删除,它只会使汇编更清晰);编译器是不可预测的野兽。-Sobjdump -d -Mintel x.o-Mintel

评论

0赞 fuz 9/28/2020
请注意,如果编译为 32 位,则可能需要指定足够新的体系结构(例如),因为所需的无分支指令是在大约 25 年前才添加的。-march=i686
1赞 fuz 9/28/2020
请注意,在这种特定情况下,不能使用无分支代码,因为编译器不能假设允许取消引用 if .因此,它必须发出一个分支,以保护这种取消引用。d->flagsa != b
2赞 user16217248 11/13/2023
我怀疑这里的罪魁祸首是逻辑算子,它需要分支进行短路评估,而不是实际的比较算子。请尝试。&&c |= (a == b) & (d->flags >> 4 & 1);
1赞 mirabilos 11/14/2023
@user16217248啊啊啊啊你是对的......并且编译器通常无法进行等价,因为短路(毕竟可能是)......好点,因此它在 GCC 3.4.6 中也是无分支 ( + ) 的,即使对于 .dcmpsetei386