使用现代编译器避免分支以提高性能?

计算科学 表现 编译
2021-12-01 21:36:06

您好,我希望我在正确的社区提出这个问题,如果您认为它不适合这里,请随时将我重定向到更好的地方。

正如我在半年前上大学时了解到的那样,现代 CPU 通常具有用于指令的大型管道,通过能够并行执行许多简单的事情来获得性能。

 // A typical code block inside a loop that I fear would clog up a pipeline.
 if(l1)     a = ...;
 elseif(l2) a = ...;
 ...
 else        a = ...;

 // A different way to write it as a sum that would avoid
 //  branching but at a possible cost of more instructions (?)
 a = (!!l1)*... + (!!l2)*... + ()*...;

现代编译器会知道如何(并允许自己)在第一种情况下避免分支,还是我作为实现者应该努力通过将我的代码规划和重写为逻辑算术表达式来帮助编译器?

我的目标是优化数字运算应用程序的速度。

2个回答

是的,如果可能,现代编译器会使用分支避免。例如,他们会将分配给变量的公共子表达式a从 if/else 分支中提取出来;然后,他们将查看分配计算中剩余的内容是否足够简单,以具有允许在不进行分支的情况下进行计算的公式表达式,或者通过依赖于条件标志而不是执行的分支指令的条件分配。如果表达式足够简单,编译器也可以使用您建议的技术,但是如果您乘以的东西足够便宜以计算所有可能的分支,那显然只是一种胜利。

但就像所有的优化一样,编译器能做的实际上是相当有限的。它通常使您免于使用微优化故意模糊代码,但它不会使您免于考虑更大的图景。

为了补充 Wolfgang 的答案,有许多表达式看起来可以通过足够智能的编译器进行优化,但是对于这种优化的一般使用是不安全的。正如您所指出的,重写条件以使用布尔算术涉及计算每个可能的分支,然后计算一种加权和;这会进行更多的计算,但不会因为分支而减慢您的速度。作为一个愚蠢的例子,这段代码:

if (x > 0)
    return x;
return -x;

可以简单地转化为

bool b = (x > 0);
return b * x + (!b) * (-x);

但是这个呢:

if (fabs(x) > eps)
    return sin(x) / x;
return 1.0;

该分支用于避免除以零,因此您不希望编译器在此处执行相同的优化。原则上,几乎任何操作都可以触发浮点异常两个双精度数相加或相乘可能会溢出。所以一个合理的优化编译器可能会选择不删除一个分支,即使你作为程序员知道里面的表达式是无害的。然后你必须自己优化它。

最后,虽然使用布尔算术消除条件可能是一个有用的技巧,但它会使您的代码的可读性大大降低,并且意图应该在注释中或记录在某处。