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();
}
}
- 我们在第二个函数中有任何性能改进吗?
- 如果使用First Function,编译器可以自行优化为Second Method吗?
这个问题的很大一部分将取决于 A
、B
和 C
的真实含义(编译器将对其进行优化,如下所示)。简单的类型,绝对不值得担心。如果它们是某种 "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 进行相同的尾调用优化,否则没有区别。
但是,如果我们将 A
、B
和 C
变成编译器无法 "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
这里我们确实看到了 func1
和 func2
之间的区别,其中 func1
将调用 A
和 B
两次——因为编译器不能不要假设调用这些函数 ONCE 将执行与调用两次相同的操作。 [考虑到函数 A
和 B
可能正在从文件中读取数据,调用 rand
或其他任何东西,不调用该函数的结果可能是程序的行为不同。
(在这种情况下,我只发布了 clang 代码,但 g++ 生成的代码具有相同的结果,但不同代码块的顺序略有不同)
下面哪个代码会更优化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();
}
}
- 我们在第二个函数中有任何性能改进吗?
- 如果使用First Function,编译器可以自行优化为Second Method吗?
这个问题的很大一部分将取决于 A
、B
和 C
的真实含义(编译器将对其进行优化,如下所示)。简单的类型,绝对不值得担心。如果它们是某种 "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 进行相同的尾调用优化,否则没有区别。
但是,如果我们将 A
、B
和 C
变成编译器无法 "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
这里我们确实看到了 func1
和 func2
之间的区别,其中 func1
将调用 A
和 B
两次——因为编译器不能不要假设调用这些函数 ONCE 将执行与调用两次相同的操作。 [考虑到函数 A
和 B
可能正在从文件中读取数据,调用 rand
或其他任何东西,不调用该函数的结果可能是程序的行为不同。
(在这种情况下,我只发布了 clang 代码,但 g++ 生成的代码具有相同的结果,但不同代码块的顺序略有不同)