[英]Optimize nested if statements within a loop in C/C++ with GCC
我正在使用GCC编译器测试C / C ++中的各种优化。 我目前有一个包含多个嵌套if语句的循环。 条件是在程序执行开始时计算的。 看起来有点像这样:
bool conditionA = getA();
bool conditionB = getB();
bool conditionC = getC();
//Etc.
startTiming();
do {
if(conditionA) {
doATrueStuff();
if(conditionB) {
//Etc.
} else {
//Etc.
}
} else {
doAFalseStuff();
if(conditionB) {
//Etc.
} else {
//Etc.
}
}
} while (testCondition());
endTiming();
doATrueStuff()
是一个内联函数,它执行一些简单的数值计算,因此调用它没有任何开销。
不幸的是,不能事先定义条件,它们必须在运行时计算。 我们甚至无法可靠地预测他们是真是假的可能性。 getA()
也可能是rand()%2
。 但经过计算,它们的价值永远不会改变。
我想到了两个解决方案,一个是全局函数指针,用于在循环中调用适当的函数,如下所示:
void (*ptrA)(void);
//Etc.
int main(int argc, char **argv) {
//...
if (conditionA) {
ptrA=&aTrueFunc;
} else {
ptrA=&aFalseFunc;
}
//...
do {
(*ptrA)();
} while (testCondition());
//...
}
这样我可以从循环中消除所有分支,但是然后我将有多个函数调用的开销减慢我的速度。
或者我可以简单地为每个条件组合设置一个不同的循环,如下所示:
if(conditionA) {
if(conditionB) {
do {
//Do A == true B == true stuff
} while (testCondition());
} else {
do {
//Do A == true B == false stuff
} while (testCondition());
}
} else {
//Etc.
}
然而,当一个人开始拥有太多条件时,这样就不那么优雅并且不可能有效地做到这一点,因为对于X条件,人们需要编写2 ^ X个循环。
是否有更优雅/更快的方式来优化它?
在这方面是否有任何意义,或者编译器是否会以某种方式理解条件在循环期间不会发生变化并自行优化?
出于好奇,有没有其他编程语言可以使编写这样的代码更容易/可能? 或者只有通过使用程序集在程序加载到内存后更改程序的指令才能实现?
考虑模板。 挑战在于将运行时值映射到编译时模板参数。 下面的样板是每个参数一个调度函数,编译器将为您创建组合树。 不完全优雅,但比开放编码多参数开关站要好得多。
您也可以直接在计算中使用模板参数(或函数),也可以优化这些参数,例如根据模板参数选择常量,或者将0乘以您不喜欢的表达式术语。我想贡献。
template <bool B0, bool B1, bool B2>
void doStuffStage3()
{
// Once you get here, you can use B0, B1, and B2 in
// any expressions you want, in the inner loop, and the compiler
// will optimize everything out since they're known compile-time. Basically,
// the compiler will create separate versions of this function
// for all required combinations of the input
do {
if(B0) {
} else {
}
} while(testCondition());
}
template <bool B0, bool B1>
void doStuffStage2(bool b2)
{
if(b2) doStuffStage3<B0,B1,true>();
else doStuffStage3<B0,B1,false>();
}
template <bool B0>
void doStuffStage1(bool b1, bool b2)
{
if(b1) doStuffStage2<B0,true> (b2);
else doStuffStage2<B0,false>(b2);
}
void doStuff(bool b0, bool b1, bool b2)
{
if(b0) doStuffStage1<true> (b1, b2);
else doStuffStage1<false>(b1, b2);
}
int main()
{
doStuff(getA(), getB(), getC());
}
理论:
尝试通过一些古怪的重写来优化代码可能会使编译器难以进行通常的优化。 编译器和处理器可以使用2种技术优化代码:
作为一个简单的开发人员,使用gcc,您还可以使用“可能”和“不太可能”的编译提示来帮助分支预测或代码生成。 点击此处了解更多详情。 如果您知道一个条件比另一个条件更可能发生,那么这可能会有效。
要查看分支预测效率,请使用perf stat ./binary并检查分支未命中率以及您执行的每个优化的分支未命中数。
在您的代码中:
如果在循环之前计算conditionA,conditionB和conditionC,并且不改变,则分支预测器很容易检测到模式。 CPU的预测器通过跟踪已采用/未采用的最后一个分支来执行此操作,并将使用记录的历史记录来预测以下分支。 因此,我实际上期望由于代码中的分支而导致性能损失很小,您可以如上所述进行验证。
2019年的快速更新。
如果涉及性能,您希望汇编代码使用“if”OUTSIDE for for循环编写。 即使使用最佳分支预测,循环内“if语句”的影响也很重要。 CPU将在每个循环上执行另外2条指令(“cmp”和“jump”)。 假设您正在处理大图像,并且您的循环遍历图像的所有像素,这可能会导致很多cpu周期。
但是,如果你按照你的方式编写代码(你显示的第一个代码),优化的(-03)gcc实际上会将条件置于循环之外并在每个分支中复制几乎相同的代码以防止在内部时效率低下你的循环。 基本上,当你懒洋洋地写第一个代码时,gcc足够聪明地写出你的第三个代码的输出:-)。 至少有两个条件。 我没有超过2个条件进行锻炼。
这种行为实际上称为循环非开关: https : //en.wikipedia.org/wiki/Loop_unswitching
// Disassemblies can be generated with
// gcc -DLAZY_WRITING -O3 -c -S main.c -o lazy.s
// gcc -O3 -c -S main.c -o notlazy.s
// -O3 is important as otherwise the condition appears in the loop
#ifdef LAZY_WRITING /* gcc will optimize*/
int do_that_big_loops()
{
int i;
int condition1 = get_condition1();
int condition2 = get_condition2();
int len = 10000;
for (i =0; i<len+1; i++)
{
call_my_func_always(i);
if (condition1)
{
if (condition2)
call_my_func_c1_c2(i);
else
call_my_func_c1_nc2(i);
}
else
{
if (condition2)
{
call_my_func_nc1_c2(i);
}
else
{
call_my_func_nc1_nc2(i);
}
}
}
return 0;
}
#else /* human-optimization */
int do_that_big_loops()
{
int i;
int condition1 = get_condition1();
int condition2 = get_condition2();
int len = 10000;
if (condition1 && condition2)
{
for (i =0; i<len+1; i++)
{
call_my_func_always(i);
call_my_func_c1_c2(i);
}
}
else if (condition1 && !condition2)
{
for (i =0; i<len+1; i++)
{
call_my_func_always(i);
call_my_func_c1_nc2(i);
}
}
else if (!condition1 && condition2)
{
for (i =0; i<len+1; i++)
{
call_my_func_always(i);
call_my_func_nc1_c2(i);
}
}
else // (!condition1 && !condition2)
{
for (i =0; i<len+1; i++)
{
call_my_func_always(i);
call_my_func_nc1_nc2(i);
}
}
return 0;
}
#endif
下面是懒惰版本的反汇编。 它几乎与非懒惰的一样(不包括在帖子中,随意使用提供的gcc命令生成它)。 您将看到对call_my_func_always()的4个不同调用,尽管实际上只有一个调用代码。
.file "main.c"
.section .text.unlikely,"ax",@progbits
.LCOLDB0:
.text
.LHOTB0:
.p2align 4,,15
.globl do_that_big_loops
.type do_that_big_loops, @function
do_that_big_loops:
.LFB0:
.cfi_startproc
pushq %rbx
.cfi_def_cfa_offset 16
.cfi_offset 3, -16
xorl %eax, %eax
call get_condition1
movl %eax, %ebx
xorl %eax, %eax
call get_condition2
testl %ebx, %ebx
jne .L2
testl %eax, %eax
je .L4
xorl %ebx, %ebx
.p2align 4,,10
.p2align 3
.L6:
movl %ebx, %edi
xorl %eax, %eax
call call_my_func_always
movl %ebx, %edi
xorl %eax, %eax
addl $1, %ebx
call call_my_func_nc1_c2
cmpl $10001, %ebx
jne .L6
.L5:
xorl %eax, %eax
popq %rbx
.cfi_remember_state
.cfi_def_cfa_offset 8
ret
.p2align 4,,10
.p2align 3
.L4:
.cfi_restore_state
movl %ebx, %edi
xorl %eax, %eax
call call_my_func_always
movl %ebx, %edi
xorl %eax, %eax
addl $1, %ebx
call call_my_func_nc1_nc2
cmpl $10001, %ebx
jne .L4
jmp .L5
.p2align 4,,10
.p2align 3
.L2:
xorl %ebx, %ebx
testl %eax, %eax
jne .L9
.p2align 4,,10
.p2align 3
.L8:
movl %ebx, %edi
xorl %eax, %eax
call call_my_func_always
movl %ebx, %edi
xorl %eax, %eax
addl $1, %ebx
call call_my_func_c1_nc2
cmpl $10001, %ebx
jne .L8
jmp .L5
.p2align 4,,10
.p2align 3
.L9:
movl %ebx, %edi
xorl %eax, %eax
call call_my_func_always
movl %ebx, %edi
xorl %eax, %eax
addl $1, %ebx
call call_my_func_c1_c2
cmpl $10001, %ebx
jne .L9
jmp .L5
.cfi_endproc
.LFE0:
.size do_that_big_loops, .-do_that_big_loops
.section .text.unlikely
.LCOLDE0:
.text
.LHOTE0:
.ident "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.10) 5.4.0 20160609"
.section .note.GNU-stack,"",@progbits
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.