11

GCC 手册仅显示了将__builtin_expect() 放置在“if”语句的整个条件周围的示例。

我还注意到,如果我使用 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;
}
4

1 回答 1

9

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

首先,让我们看一下以下三个代码示例,其中两个同时使用__builtin_expect正则 if 和三元 if 样式,第三个根本不使用它。

内置.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);
}

三元.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 都具有相同的程序集列表(重要的地方):

内置.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

三元:

    .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

相关部分:

差异

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

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

于 2013-02-09T05:57:06.950 回答