在模数之前和分配操作之前,`if`语句是否冗余?

时间:2017-05-02 06:10:19

标签: c++ c performance arm thumb

考虑下一个代码:

unsigned idx;
//.. some work with idx
if( idx >= idx_max )
    idx %= idx_max;

可以简化为第二行:

idx %= idx_max;

并将实现相同的结果。

我多次遇到下一个代码:

unsigned x;
//... some work with x
if( x!=0 )
  x=0;

可以简化为

x=0;

问题:

  • 使用if有什么意义吗?为什么?特别是使用ARM Thumb指令集。
  • 这些if可以省略吗?
  • 编译器有什么优化?

4 个答案:

答案 0 :(得分:66)

如果你想了解编译器正在做什么,你需要提取一些程序集。我推荐这个网站(我已经从问题中输入了代码)):https://godbolt.org/g/FwZZOb

第一个例子更有趣。

int div(unsigned int num, unsigned int num2) {
    if( num >= num2 ) return num % num2;
    return num;
}

int div2(unsigned int num, unsigned int num2) {
    return num % num2;
}

生成:

div(unsigned int, unsigned int):          # @div(unsigned int, unsigned int)
        mov     eax, edi
        cmp     eax, esi
        jb      .LBB0_2
        xor     edx, edx
        div     esi
        mov     eax, edx
.LBB0_2:
        ret

div2(unsigned int, unsigned int):         # @div2(unsigned int, unsigned int)
        xor     edx, edx
        mov     eax, edi
        div     esi
        mov     eax, edx
        ret

基本上,由于非常具体和逻辑的原因,编译器将优化掉分支。如果整数除法与比较的成本大致相同,那么分支将是毫无意义的。但整数除法(模数与典型值一起执行)实际上非常昂贵:http://www.agner.org/optimize/instruction_tables.pdf。这些数字因架构和整数大小而异,但通常可能是从15到接近100个周期的延迟。

通过在执行模数之前选择分支,您实际上可以节省大量的工作。请注意:编译器也不会将没有分支的代码转换为程序集级别的分支。那是因为分支也有缺点:如果最终需要模数,那你只是浪费了一点时间。

在不知道idx < idx_max将成为真的相对频率的情况下,无法对正确的优化做出合理的决定。所以编译器(gcc和clang做同样的事情)选择以相对透明的方式映射代码,将这个选择留给开发人员。

所以这个分支可能是一个非常合理的选择。

第二个分支应该完全没有意义,因为比较和分配 的成本相当。也就是说,您可以在链接中看到,如果编译器具有对变量的引用,则仍然不会执行此优化。如果值是局部变量(如在演示的代码中那样),那么编译器将优化分支。

总之,第一段代码可能是合理的优化,第二段,可能只是一个累了的程序员。

答案 1 :(得分:7)

在许多情况下,使用已保存的值编写变量可能比读取它更慢,找出已保持所需的值,并跳过写入。某些系统具有处理器缓存,可立即将所有写入请求发送到内存。虽然这些设计在今天并不常见,但它们过去很常见,因为它们可以提供完全读/写缓存所能提供的大部分性能提升,但成本只占很小一部分。

如上所述的代码在一些多CPU情况下也可能是相关的。最常见的情况是在两个或多个CPU核心上同时运行的代码将重复命中变量。在具有强大内存模型的多核缓存系统中,想要编写变量的核心必须首先与其他核心协商以获取包含它的缓存行的独占所有权,然后必须再次协商以在下次放弃此类控制时任何其他核心都想读或写它。这样的操作往往非常昂贵,即使每次写入只是存储已经存储的值,也必须承担成本。但是,如果该位置变为零并且永远不会再次写入,则两个内核可以同时保留高速缓存行以进行非独占的只读访问,并且永远不必进一步协商它。

在几乎所有多个CPU都可以命中变量的情况下,变量至少应该声明为volatile。可能适用的一个例外是,在main()开始之后发生的对变量的所有写入都将存储相同的值,并且无论一个CPU是否存储任何存储,代码都将正常运行在另一个可见。如果多次执行某些操作会浪费但是无害,并且变量的目的是说是否需要完成,那么许多实现可能能够在没有volatile限定符的情况下生成更好的代码,而不是只要他们不通过使写作无条件来提高效率。

顺便提一下,如果通过指针访问对象,则会有另一个 上述代码的可能原因:如果函数被设计为接受任何一个 某个字段为零的const对象,或者非const个对象 应该将该字段设置为零,可能需要像上面这样的代码 确保两种情况下的定义行为。

答案 2 :(得分:2)

关注第一段代码:这是基于Chandler Carruth对Clang的推荐的微优化(有关详细信息,请参阅here),但它并不一定认为它会是这种形式的有效微优化(使用if而不是三元)或任何给定的编译器。

Modulo是一个相当昂贵的操作,如果代码经常被执行并且有一个强大的统计倾向于一个或另一个条件,CPU的分支预测(给定一个现代CPU)将显着降低分支指令的成本。

答案 3 :(得分:1)

对我来说使用if if似乎是一个坏主意。

你是对的。无论是idx >= idx_max,还是idx %= idx_max之后的idx_max。如果idx < idx_max,它将保持不变,是否遵循if。

虽然您可能认为围绕模数进行分支可能会节省时间,但我认为,真正的罪魁祸首是,当遵循分支时,流水线现代CPU必须重置其管道,并且这需要花费相对大量的时间。最好不要遵循一个分支,而不是一个整数模数,这大约和整数除法一样多。

编辑:事实证明,模数对分支的速度相当慢,正如其他人所建议的那样。这是一个人正在研究这个完全相同的问题:CppCon 2015: Chandler Carruth "Tuning C++: Benchmarks, and CPUs, and Compilers! Oh My!"(在另一个SO问题中建议与此问题的另一个答案相关联。)

这家伙写编译器,并认为没有分支会更快;但他的基准证明他错了。即使分支机构仅占20%的时间,它的测试速度也更快。

没有if的另一个原因:维护一行代码,并让别人弄清楚它意味着什么。上面链接中的人实际上创建了一个“更快模数”的宏。恕我直言,这个或内联函数是性能关键型应用程序的方法,因为没有分支,你的代码将更容易理解,但执行速度会快。

最后,上面视频中的人正计划让编译器编写者知道这种优化。因此,如果不在代码中,可能会为您添加if。因此,当这个问题出现时,仅仅是mod就可以了。