if语句在模数之前和赋值操作之前是多余的吗?

考虑下一个代码:

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被忽略?
  • 编译器做了哪些优化?

如果你想了解编译器在做什么,你只需要拉起一些程序集。 我推荐这个网站(我已经input了代码): 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做同样的事情)select以相对透明的方式映射代码,将这个select留在开发者手中。

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

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

总而言之,第一个代码可能是一个合理的优化,第二个代码可能只是一个疲惫的程序员。

有很多情况下,用已经存储的值编写一个variables可能比读取它慢,发现已经保存了所需的值,并跳过写入。 一些系统有一个处理器caching,可以立即将所有写入请求发送到内存。 虽然这样的devise在今天并不普遍,但是它们以前是相当常见的,因为它们可以提供完整读/写caching可以提供的性能提升的相当大的一部分,但是成本的一​​小部分。

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

在几乎所有情况下,多个CPU都可能触及一个variables,variables应该至less被声明为volatile 。 在这里可能适用的一个例外情况是,在main()开始之后发生的variables的所有写入都将存储相同的值,并且无论一个CPU的任何存储是否可见在另一个。 如果多次执行一些操作将是浪费的,但是否则是无害的,并且variables的目的是说明是否需要完成,那么许多实现可以在没有volatile限定符的情况下生成更好的代码,不要试图通过使写无条件来提高效率。

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

关心第一块代码:这是一个基于Chandler Carruth对Clang的推荐的微观优化(详见这里了解更多信息),但是并不一定认为这将是一种有效的微型优化(如果宁愿比三元)或任何给定的编译器。

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

如果有,对我来说,这似乎是一个坏主意。

你是对的。 无论idx >= idx_max ,它将在idx %= idx_max之后的idx %= idx_max 。 如果idx < idx_max ,它将不会改变,if是否被跟踪。

虽然你可能认为在模数上分支可能会节省时间,但我想说,真正的罪魁祸首是,当遵循分支时,现代CPU的stream水线必须重新设置stream水线,并且花费相当多的时间。 最好不要跟随一个分支,而不是一个整数模,它的花费和整数除法一样多。

编辑:事实certificate,模数是非常缓慢的分支,正如其他人在这里所build议的。 下面是一个检查同样问题的人: CppCon 2015:Chandler Carruth“调优C ++:基准,CPU和编译器!哦,我的! (在另一个与这个问题的答案有关的SO问题中提出)。

这个人写编译器,并认为没有分支会更快; 但他的基准certificate他是错的。 即使只有20%的时间分支,它testing更快。

另一个原因是没有:如果没有一行代码要维护,而另外一个人想弄清楚它的意思。 上面的链接中的家伙实际上创build了一个“更快的模数”macros。 恕我直言,这或内联function是去关键性能的应用程序的方式,因为你的代码会更容易理解没有分支,但将执行速度。

最后,上述video中的人正在计划让编译器作者知道这种优化。 因此,if可能会添加给你,如果不是在代码中。 因此,当这个问题出现的时候,只有mod才会这样做。