繁体   English   中英

使用GCC优化C / C ++中循环内的嵌套if语句

[英]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种技术优化代码:

  1. 分支预测:编译器可以通过使用配置文件引导的优化来实现 ,主要是通过估计每个分支的概率。 除了计算每个目标的统计信息之外,CPU还具有尝试检测分支模式的分支目标缓冲区。
  2. 分支预测:编译器或CPU将使代码并行执行两个分支( 因为现在处理器是超标量 )并且基于条件结果,它将忽略不正确路径的结果(例如CMOV指令)。 您可以尝试使用以下命令禁用分支预测:-fno-if-conversion和-fno-if-conversion2 如果每个分支上有大量计算并且执行所有路径将导致指令解码器和执行端口的浪费,这可能会有所帮助。

作为一个简单的开发人员,使用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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM