【问题标题】:if-else if ladder and Compiler Optimizationif-else if 阶梯和编译器优化
【发布时间】:2015-04-15 12:35:17
【问题描述】:

以下哪个代码在 C/C++ gcc 编译器中First functionSecond function的效率会更高?

// First Function
if ( A && B && C ) {
    UpdateData();
} else if ( A && B ){
    ResetData();
}

//Second Function
if ( A && B) {
    if (C) {
        UpdateData();
    } else {
        ResetData();
    }
}
  1. 我们在 第二功能 中是否有任何性能改进?
  2. 如果使用First Function,编译器能否自行优化为Second Method

【问题讨论】:

  • 您可以查看已编译的代码,但我敢打赌它们会编译成完全相同的代码。这也闻起来像微优化,只要写最易读的就行了。
  • 为什么标记为 C? C 没有方法。
  • 我敢打赌,您没有通过分析发现这个分支事物是您的瓶颈,也没有测量两种可能性之间的切换。
  • 我在我的(假设的)计算机上都运行,第一个在运行 2,653,456 次时表现更好;第二个在运行 2,642,391 次时表现更好。但请注意,每个总运行时间的差异小于 0.000007362%!
  • 您实际上是在询问优化编译器是否可以推断将AB 转换为布尔表达式是否会产生一致的结果而没有副作用。答案是“可能”。

标签: c++ c gcc


【解决方案1】:

这个问题的很大一部分将取决于ABC 到底是什么(编译器会对其进行优化,如下所示)。简单的类型,绝对不值得担心。如果它们是某种“大数数学”对象,或者是某种复杂的数据类型,每个需要 1000 条指令“这是true 还是不是”,那么如果编译器决定编写不同的代码,就会有很大的不同。

与以往一样,在性能方面:在您自己的代码中进行测量,使用分析来检测代码花费最多时间的位置,然后通过对该代码的更改进行测量。重复直到它运行得足够快[不管那是什么]和/或你的经理告诉你停止摆弄代码。然而,通常情况下,除非它确实是代码的高流量区域,否则重新安排 if 语句中的条件几乎没有什么区别,它是在一般情况下产生最大影响的整体算法。

如果我们假设 A、B 和 C 是简单的类型,比如int,我们可以写一些代码来调查:

extern int A, B, C;
extern void UpdateData();
extern void ResetData();

void func1()
{
    if ( A && B && C ) {
        UpdateData();
    } else if ( A && B ){
        ResetData();
    }
}


void func2()
{
    if ( A && B) {
        if (C) {
            UpdateData();
        } else {
            ResetData();
        }
    }
}

gcc 4.8.2 给出这个,使用 -O1 产生这个代码:

_Z5func1v:
    cmpl    $0, A(%rip)
    je  .L6
    cmpl    $0, B(%rip)
    je  .L6
    subq    $8, %rsp
    cmpl    $0, C(%rip)
    je  .L3
    call    _Z10UpdateDatav
    jmp .L1
.L3:
    call    _Z9ResetDatav
.L1:
    addq    $8, %rsp
.L6:
    rep ret

_Z5func2v:
.LFB1:
    cmpl    $0, A(%rip)
    je  .L12
    cmpl    $0, B(%rip)
    je  .L12
    subq    $8, %rsp
    cmpl    $0, C(%rip)
    je  .L9
    call    _Z10UpdateDatav
    jmp .L7
.L9:
    call    _Z9ResetDatav
.L7:
    addq    $8, %rsp
.L12:
    rep ret

换句话说:完全没有区别

使用带有 -O1 的 clang++ 3.7(截至大约 3 周前)给出以下结果:

_Z5func1v:                              # @_Z5func1v
    cmpl    $0, A(%rip)
    setne   %cl
    cmpl    $0, B(%rip)
    setne   %al
    andb    %cl, %al
    movzbl  %al, %ecx
    cmpl    $1, %ecx
    jne .LBB0_2
    movl    C(%rip), %ecx
    testl   %ecx, %ecx
    je  .LBB0_2
    jmp _Z10UpdateDatav         # TAILCALL
.LBB0_2:                                # %if.else
    testb   %al, %al
    je  .LBB0_3
    jmp _Z9ResetDatav           # TAILCALL
.LBB0_3:                                # %if.end8
    retq

_Z5func2v:                              # @_Z5func2v
    cmpl    $0, A(%rip)
    je  .LBB1_4
    movl    B(%rip), %eax
    testl   %eax, %eax
    je  .LBB1_4
    cmpl    $0, C(%rip)
    je  .LBB1_3
    jmp _Z10UpdateDatav         # TAILCALL
.LBB1_4:                                # %if.end4
    retq
.LBB1_3:                                # %if.else
    jmp _Z9ResetDatav           # TAILCALL
.Ltmp1:

在 clang 的 func1 中链接和可能是有益的,但它可能是一个很小的差异,你应该专注于从代码的逻辑角度来看更有意义的地方。

总结:不值得

g++ 中更高的优化使其执行与 clang 相同的尾调用优化,否则没有区别。

但是,如果我们将ABC 变成编译器无法“理解”的外部函数,那么我们就会有所不同:

_Z5func1v:                              # @_Z5func1v
    pushq   %rax
.Ltmp0:
    .cfi_def_cfa_offset 16
    callq   _Z1Av
    testl   %eax, %eax
    je  .LBB0_3

    callq   _Z1Bv
    testl   %eax, %eax
    je  .LBB0_3

    callq   _Z1Cv
    testl   %eax, %eax
    je  .LBB0_3

    popq    %rax
    jmp _Z10UpdateDatav         # TAILCALL
.LBB0_3:                                # %if.else
    callq   _Z1Av
    testl   %eax, %eax
    je  .LBB0_5

    callq   _Z1Bv
    testl   %eax, %eax
    je  .LBB0_5

    popq    %rax
    jmp _Z9ResetDatav           # TAILCALL
.LBB0_5:                                # %if.end12
    popq    %rax
    retq

_Z5func2v:                              # @_Z5func2v
    pushq   %rax
.Ltmp2:
    .cfi_def_cfa_offset 16
    callq   _Z1Av
    testl   %eax, %eax
    je  .LBB1_4

    callq   _Z1Bv
    testl   %eax, %eax
    je  .LBB1_4

    callq   _Z1Cv
    testl   %eax, %eax
    je  .LBB1_3

    popq    %rax
    jmp _Z10UpdateDatav         # TAILCALL
.LBB1_4:                                # %if.end6
    popq    %rax
    retq
.LBB1_3:                                # %if.else
    popq    %rax
    jmp _Z9ResetDatav           # TAILCALL

在这里我们确实看到了func1func2 之间的区别,其中func1 将调用AB 两次——因为编译器不能假设调用这些函数ONCE 会做同样的事情作为调用两次。 [考虑到函数AB 可能正在从文件中读取数据,调用rand,或者其他什么,不调用该函数的结果可能是程序的行为不同。

(在这种情况下,我只发布了 clang 代码,但 g++ 生成的代码具有相同的结果,但不同代码块的顺序略有不同)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-12-31
    • 2020-11-23
    • 2019-12-08
    • 2013-09-10
    • 2023-03-28
    • 1970-01-01
    相关资源
    最近更新 更多