【问题标题】:Can I use GCC's __builtin_expect() with ternary operator in C我可以在 C 中将 GCC 的 __builtin_expect() 与三元运算符一起使用吗
【发布时间】:2013-01-24 22:07:41
【问题描述】:

GCC manual 仅显示了将 __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;
}

【问题讨论】:

    标签: c gcc ternary-operator branch-prediction


    【解决方案1】:

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

    首先,让我们看一下以下三个代码示例,其中两个在常规-if 和三元-if 样式中都使用__builtin_expect,而第三个则根本不使用它。

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

    三元.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
    

    相关部分:

    基本上,__builtin_expect 会导致额外的代码 (sete %al...) 在 je .L2 之前执行,基于testl %eax, %eax 的结果,CPU 更有可能预测为 1(天真的假设,这里)而不是基于输入字符与'c' 的直接比较。而在 nobuiltin.c 的情况下,不存在这样的代码,je/jne 直接跟在与 'c' (cmp $99) 的比较之后。请记住,分支预测主要是在 CPU 中完成的,这里 GCC 只是简单地“设下陷阱”让 CPU 分支预测器假设将采用哪条路径(通过额外的代码以及 jejne 的切换,虽然我没有这方面的消息来源,因为英特尔的official optimization manual 没有提到用jejne 对第一次遭遇进行不同的分支预测!我只能假设GCC 团队通过反复试验得出了这个结论) .

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

    【讨论】:

    • 非常好的分析和非常好的结果展示。感谢您的努力。
    • 除了__builtin_expect 对x86 的优化代码没有影响(因为你说它们与-O3 相同)之外,这并没有真正显示任何东西。它们之前不同的唯一原因是__builtin_expect 是一个返回给它的值的函数,并且该返回值不能通过标志发生。否则,差异将留在优化后的代码中。
    • @ughoavgfhw:“返回值不能通过标志发生”是什么意思?
    • @Kristian 调用约定不允许返回值由标志寄存器中的位指示,这就是未优化代码需要sete %al 的原因。它是返回比较结果的内置函数。
    • __builtin_expect 很可能(嗯,根据您的代码,凭经验)对如此简单的一段代码是无操作的,尤其是在 x86 上。您应该尝试一段不太可能的代码路径执行大量附加指令的代码,并查看编译器是否足够聪明以将其移出热路径。 (在 x86 上,分支预测器非常好,使用 __builtin_expect 的唯一原因是缩小热路径的 icache 占用空间。)您也可以尝试为 ARM 或 PPC 进行编译,它们更有可能具有特殊的编译器逻辑致力于欺骗分支预测器。
    猜你喜欢
    • 2018-04-17
    • 2016-08-22
    • 2011-01-13
    • 2023-02-16
    • 2019-01-06
    • 2019-05-26
    • 2019-10-08
    • 2020-10-21
    • 2020-05-09
    相关资源
    最近更新 更多