我能使用GCC的__builtin_expect()函数与C语言中的三目运算符一起使用吗?

11

GCC手册只展示了在'if'语句的整个条件周围使用__builtin_expect()的示例。

我还注意到,例如在三元运算符中使用它,或者在任意整数表达式中使用它(即使不用在分支上下文中),GCC也不会抱怨。

因此,我想知道其使用的基本约束条件是什么。

当像这样在三元操作中使用时,它是否会保留其效果:

int foo(int i)
{
  return __builtin_expect(i == 7, 1) ? 100 : 200;
}

那么这种情况呢:

int foo(int i)
{
  return __builtin_expect(i, 7) == 7 ? 100 : 200;
}

还有这一个:

int foo(int i)
{
  int j = __builtin_expect(i, 7);
  return j == 7 ? 100 : 200;
}
1个回答

9

它似乎可以同时用于三元和普通的if语句。

首先,让我们看一下以下三个代码示例,其中两个在普通if和三元if样式中都使用了 __builtin_expect ,而第三个则完全没有使用。

builtin.c:

int main()
{
    char c = getchar();
    const char *printVal;
    if (__builtin_expect(c == 'c', 1))
    {
        printVal = "Took expected branch!\n";
    }
    else
    {
        printVal = "Boo!\n";
    }

    printf(printVal);
}

ternary.c:

int main()
{
    char c = getchar();
    const char *printVal = __builtin_expect(c == 'c', 1) 
        ? "Took expected branch!\n"
        : "Boo!\n";

    printf(printVal);
}

nobuiltin.c:

int main()
{
    char c = getchar();
    const char *printVal;
    if (c == 'c')
    {
        printVal = "Took expected branch!\n";
    }
    else
    {
        printVal = "Boo!\n";
    }

    printf(printVal);
}

当使用-O3编译时,这三个代码生成的汇编代码是相同的。但是,当省略-O(在GCC 4.7.2上)时,无论是ternary.c还是builtin.c在关键位置都具有相同的汇编代码:

builtin.s:

    .file   "builtin.c"
    .section    .rodata
.LC0:
    .string "Took expected branch!\n"
.LC1:
    .string "Boo!\n"
    .text
    .globl  main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    pushl   %ebp
    .cfi_def_cfa_offset 8
    .cfi_offset 5, -8
    movl    %esp, %ebp
    .cfi_def_cfa_register 5
    andl    $-16, %esp
    subl    $32, %esp
    call    getchar
    movb    %al, 27(%esp)
    cmpb    $99, 27(%esp)
    sete    %al
    movzbl  %al, %eax
    testl   %eax, %eax
    je  .L2
    movl    $.LC0, 28(%esp)
    jmp .L3
.L2:
    movl    $.LC1, 28(%esp)
.L3:
    movl    28(%esp), %eax
    movl    %eax, (%esp)
    call    printf
    leave
    .cfi_restore 5
    .cfi_def_cfa 4, 4
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .ident  "GCC: (Debian 4.7.2-4) 4.7.2"
    .section    .note.GNU-stack,"",@progbits

ternary.s:

    .file   "ternary.c"
    .section    .rodata
.LC0:
    .string "Took expected branch!\n"
.LC1:
    .string "Boo!\n"
    .text
    .globl  main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    pushl   %ebp
    .cfi_def_cfa_offset 8
    .cfi_offset 5, -8
    movl    %esp, %ebp
    .cfi_def_cfa_register 5
    andl    $-16, %esp
    subl    $32, %esp
    call    getchar
    movb    %al, 31(%esp)
    cmpb    $99, 31(%esp)
    sete    %al
    movzbl  %al, %eax
    testl   %eax, %eax
    je  .L2
    movl    $.LC0, %eax
    jmp .L3
.L2:
    movl    $.LC1, %eax
.L3:
    movl    %eax, 24(%esp)
    movl    24(%esp), %eax
    movl    %eax, (%esp)
    call    printf
    leave
    .cfi_restore 5
    .cfi_def_cfa 4, 4
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .ident  "GCC: (Debian 4.7.2-4) 4.7.2"
    .section    .note.GNU-stack,"",@progbits

相比之下,nobuiltin.c并不会:

    .file   "nobuiltin.c"
    .section    .rodata
.LC0:
    .string "Took expected branch!\n"
.LC1:
    .string "Boo!\n"
    .text
    .globl  main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    pushl   %ebp
    .cfi_def_cfa_offset 8
    .cfi_offset 5, -8
    movl    %esp, %ebp
    .cfi_def_cfa_register 5
    andl    $-16, %esp
    subl    $32, %esp
    call    getchar
    movb    %al, 27(%esp)
    cmpb    $99, 27(%esp)
    jne .L2
    movl    $.LC0, 28(%esp)
    jmp .L3
.L2:
    movl    $.LC1, 28(%esp)
.L3:
    movl    28(%esp), %eax
    movl    %eax, (%esp)
    call    printf
    leave
    .cfi_restore 5
    .cfi_def_cfa 4, 4
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .ident  "GCC: (Debian 4.7.2-4) 4.7.2"
    .section    .note.GNU-stack,"",@progbits

相关部分:

diff

基本上,__builtin_expect 会导致额外的代码(例如 sete %al ...)在根据CPU更有可能预测为1(这里是一种天真的假设)而不是基于输入字符与 'c' 的直接比较之前执行。然而,在nobuiltin.c中,没有这样的代码存在,je/jne 直接跟随与 'c' 的比较(cmp $99)。请记住,分支预测主要由CPU完成,在这里GCC只是为CPU分支预测器“设置陷阱”,以假定将采取哪个路径(通过额外的代码和 jejne 的切换,尽管我没有来源证实这一点,因为英特尔的官方优化手册没有提到将首次遇到的 jejne 与分支预测不同对待!我只能推断GCC团队通过试错得出了这一结论)。

我相信有更好的测试用例可以更直接地看到GCC的分支预测(而不是观察CPU的提示),但我不知道如何简洁地模拟这样的情况。(猜测:在编译期间可能涉及循环展开。)


非常好的分析,结果展示也非常棒。感谢您的努力。 - Kristian Spangsege
2
这实际上并没有展示除了__builtin_expect对于x86优化代码没有影响(因为你说它们在-O3下是相同的)之外的任何内容。它们之前唯一不同的原因是__builtin_expect是一个返回给定值的函数,而该返回值不能通过标志发生。否则,差异将保留在优化代码中。 - ughoavgfhw
@ughoavgfhw:你说的“返回值不能通过标志位实现”是什么意思? - Kristian Spangsege
@Kristian 调用约定不允许通过标志寄存器中的位指示返回值,这就是为什么未经优化的代码需要 sete %al。它是内置函数,返回比较结果。 - ughoavgfhw
__builtin_expect 在这样一个简单的代码片段上可能是无操作(根据您的代码经验而言)。特别是在 x86 上。您应该尝试一些代码片段,其中不太可能执行许多其他指令的代码路径,并查看编译器是否足够聪明,将其移出热路径。(在 x86 上,分支预测器非常好,使用 __builtin_expect 的唯一原因是缩小热路径的 icache 占用空间。)您还可以尝试为 ARM 或 PPC 编译,这更有可能具有专门用于欺骗分支预测器的特殊编译器逻辑。 - Quuxplusone

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接