if-else if ladder 和编译器优化

if-else if ladder and Compiler Optimization

下面哪个代码会更优化C/C++ gcc 中的第一个函数第二个函数编译器 ?

// 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吗?

这个问题的很大一部分将取决于 ABC 的真实含义(编译器将对其进行优化,如下所示)。简单的类型,绝对不值得担心。如果它们是某种 "big number math" 对象,或者某种复杂的数据类型,每个 "is this true or not" 需要 1000 条指令,那么如果编译器决定编写不同的代码,就会有很大的不同。

在性能方面一如既往:在您自己的代码中进行测量,使用分析来检测代码花费大部分时间的地方,然后通过对该代码的更改进行测量。重复直到它运行得足够快 [不管是什么] and/or 你的经理告诉你停止摆弄代码。然而,通常情况下,除非它真的是代码的高流量区域,否则在 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    [=11=], A(%rip)
    je  .L6
    cmpl    [=11=], B(%rip)
    je  .L6
    subq    , %rsp
    cmpl    [=11=], C(%rip)
    je  .L3
    call    _Z10UpdateDatav
    jmp .L1
.L3:
    call    _Z9ResetDatav
.L1:
    addq    , %rsp
.L6:
    rep ret

_Z5func2v:
.LFB1:
    cmpl    [=11=], A(%rip)
    je  .L12
    cmpl    [=11=], B(%rip)
    je  .L12
    subq    , %rsp
    cmpl    [=11=], C(%rip)
    je  .L9
    call    _Z10UpdateDatav
    jmp .L7
.L9:
    call    _Z9ResetDatav
.L7:
    addq    , %rsp
.L12:
    rep ret

换句话说:完全没有区别

使用带有 -O1 的 clang++ 3.7(大约 3 周前)给出:

_Z5func1v:                              # @_Z5func1v
    cmpl    [=12=], A(%rip)
    setne   %cl
    cmpl    [=12=], B(%rip)
    setne   %al
    andb    %cl, %al
    movzbl  %al, %ecx
    cmpl    , %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    [=12=], A(%rip)
    je  .LBB1_4
    movl    B(%rip), %eax
    testl   %eax, %eax
    je  .LBB1_4
    cmpl    [=12=], 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 变成编译器无法 "understand" 的外部函数,那么我们就会有所不同:

_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++ 生成的代码具有相同的结果,但不同代码块的顺序略有不同)