我正在使用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 :(得分:2)
考虑模板。挑战在于将运行时值映射到编译时模板参数。下面的样板是每个参数一个调度函数,编译器将为您创建组合树。不完全优雅,但比开放编码多参数开关站要好得多。
您也可以直接在计算中使用模板参数(或函数),也可以优化这些参数,例如根据模板参数选择常量,或者将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());
}
答案 1 :(得分:2)
理论:
尝试通过一些古怪的重写来优化代码可能会使编译器难以进行通常的优化。编译器和处理器可以使用两种技术优化代码:
作为一名使用gcc的简单开发人员,您还可以使用 &#34;可能&#34;来帮助分支预测或代码生成。和&#34;不太可能&#34; 编译提示。有关详细信息,请查看 here 。如果你知道一个条件比另一个条件发生的可能性更大,那么这可能会有效。
要查看分支预测效率,请使用 perf stat ./binary 并查看分支未命中率以及您执行的每项优化的分支未命中数。
在您的代码案例中:
如果在循环之前计算conditionA,conditionB和conditionC,并且不更改,则分支预测器很容易检测到模式。 CPU的预测器通过跟踪已采用/未采用的最后一个分支来做到这一点,并且它将使用记录的历史来预测以下分支。因此,我实际上期望由于代码中的分支而导致性能损失很小,您可以如上所述进行验证。
答案 2 :(得分:0)
2019年的快速更新。
如果考虑到性能,则希望用for循环之外的“ if”编写汇编代码。即使使用最佳分支预测,循环内“ if语句”的影响也可能很重要。 CPU将在每个循环上再执行2条指令(“ cmp”和“ jump”)。假设您正在处理大型图像,并且循环遍历图像的所有像素,则这可能会导致很多cpu周期。
但是,如果您以自己的方式编写代码(显示的第一个代码),则经过优化的(-03)gcc实际上会将条件置于循环之外,并在每个分支中复制几乎相同的代码,以防止出现如果在循环中,效率低下。基本上,gcc足够聪明,可以在懒惰地编写第一个:-)时编写第三个代码的输出。至少有两个条件。我没有在两个以上的条件下进行锻炼。
此行为实际上称为循环取消切换: 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