【问题标题】:Optimize nested if statements within a loop in C/C++ with GCC使用 GCC 优化 C/C++ 循环中的嵌套 if 语句
【发布时间】:2015-07-27 15:43:07
【问题描述】:

我正在使用 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 循环。

有没有更优雅/更快的方法来优化它?

这是否有任何意义,或者编译器会以某种方式理解条件在循环期间不会改变并自行优化?

出于好奇,是否有另一种编程语言可以使编写此类代码更容易/可能?还是只有在程序加载到内存后使用汇编来更改程序的指令才能实现?

【问题讨论】:

  • 第一个想法似乎没有比原来更多的函数调用。
  • 如果条件在循环内没有改变,CPU 可能会很好地进行分支预测。
  • 看来您已经拥有了 2^X 个不同的块。
  • 关于编译器的作用,我建议您在 gcc 命令行中添加 -fdump-tree-optimized 并查看生成的文件。它以相当易读的格式显示高级优化的结果。
  • 在您花费大量时间尝试此类微优化之前,明智的做法是评估是否有任何显着收益。例如,如果你完全消除条件(选择特定的条件值模式),你能测试它的速度有多快吗?

标签: c++ c loops gcc optimization


【解决方案1】:

理论:

尝试通过一些古怪的重写来优化您的代码可能会使编译器难以进行通常的优化。编译器和处理器可以使用两种技术优化代码:

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

作为一个简单的开发人员,使用 gcc,您还可以使用 “可能”和“不太可能” 编译提示来帮助进行分支预测或代码生成。查看 here 了解更多详情。例如,如果您知道一种情况比另一种情况更有可能发生,这可能会起作用。

要查看分支预测效率,请使用 perf stat ./binary 并查看分支未命中率以及每次优化的分支未命中数。

在您的代码案例中:

如果条件A、条件B和条件C在循环之前计算,并且不改变,那么分支预测器很容易检测到模式。 CPU 的预测器通过跟踪最后采用/未采用的分支来做到这一点,它将使用记录的历史来预测以下分支。因此,我实际上预计由于代码中的分支而导致的性能损失很小,您可以按上述方式进行验证。

【讨论】:

  • 您的回答非常有帮助且正确。我做了一些测试,看看分支需要多少时间,而且时间不多,所以我尝试的任何优化都不应该显着改善事情。再次感谢您提醒我注意许多我不知道的事情。
【解决方案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());
}

【讨论】:

  • 即使您的答案同样正确,我还是会选择 VAndrei 的,因为我发现它提供的信息更有帮助。不过,感谢您抽出宝贵时间回答并让我注意到这项技术。
【解决方案3】:

2019 年快速更新。

如果考虑性能,您希望在 for 循环之外使用“if”编写汇编代码。循环内的“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

下面是lazy版本的反汇编。它与非懒惰的几乎相同(未包含在帖子中,请随意使用提供的 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

【讨论】:

    猜你喜欢
    • 2017-06-16
    • 1970-01-01
    • 1970-01-01
    • 2019-08-23
    • 2015-05-29
    • 2013-05-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多