我可以在C中使用GCC的__builtin_expect()和三元运算符

Kri*_*ege 11 c gcc ternary-operator branch-prediction

GCC手册仅示出了其中__builtin_expect()被置于围绕"如果"语句的所有条件的示例.

我还注意到GCC不会抱怨,如果我使用它,例如,使用三元运算符,或任何任意积分表达式,即使是未在分支上下文中使用的表达式.

所以,我想知道它的实际使用的基本限制是什么.

在这样的三元运算中使用时是否会保持其效果:

int foo(int i)
{
  return __builtin_expect(i == 7, 1) ? 100 : 200;
}
Run Code Online (Sandbox Code Playgroud)

那么这个案子呢:

int foo(int i)
{
  return __builtin_expect(i, 7) == 7 ? 100 : 200;
}
Run Code Online (Sandbox Code Playgroud)

还有这个:

int foo(int i)
{
  int j = __builtin_expect(i, 7);
  return j == 7 ? 100 : 200;
}
Run Code Online (Sandbox Code Playgroud)

Mah*_*dsi 8

它显然适用于三元和常规if语句.

首先,我们来看看以下三个代码示例,其中两个代码使用__builtin_expectregular-if和trinary-if样式,第三个代码样本根本不使用它.

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);
}
Run Code Online (Sandbox Code Playgroud)

ternary.c:

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

    printf(printVal);
}
Run Code Online (Sandbox Code Playgroud)

nobuiltin.c:

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

    printf(printVal);
}
Run Code Online (Sandbox Code Playgroud)

编译时-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
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

而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
Run Code Online (Sandbox Code Playgroud)

相关部分:

DIFF

基本上,__builtin_expect导致额外的代码(sete %al...)在je .L2基于testl %eax, %eaxCPU更可能预测为1(这里的天真假设)的结果之前执行,而不是基于输入char的直接比较'c'.而在nobuiltin.c的情况下,不存在这样的代码,并且je/ jne直接跟随与'c'(cmp $99)的比较.请记住,分支预测主要在CPU中完成,这里GCC只是为CPU分支预测器"设置一个陷阱"来假设将采用哪条路径(通过额外的代码和切换,je并且jne,虽然我没有这是因为英特尔的官方优化手册没有提到用分支预测处理第一次遇到jevs jne不同的事情!我只能假设GCC团队通过反复试验来达到这个目的.

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

  • 除了`__builtin_expect`对x86的优化代码没有影响(因为你说它们与-O3相同)之外,这并没有真正显示任何其他内容。它们之前不同的唯一原因是`__builtin_expect` 是一个函数,它返回给它的值,并且该返回值不能通过标志发生。否则,差异将保留在优化代码中。 (2认同)