我有过这样的经验(这不是问题,而是一句话),即避免使用非常量的局部变量而使用常量变量或完全避免使用局部变量,使c++编译器能够生成更快的代码。
我认为,这给了编译器更多的自由来交错计算表达式,而赋值则迫使编译器插入一个同步点。
事实上是这样的假设吗?
还有其他解释吗?例如,一旦代码变得过于复杂,编译器就会放弃某些优化级别,以避免天文数字的编译时间?
否,赋值不会强制编译器插入同步点。如果变量是局部的,并且不影响函数外的任何可见的东西,编译器将删除所有不需要的变量,作为通常"的一部分;寄存器分配";优化。
如果您的代码过于复杂,以至于接近编译器在内存中所能保留的极限,那么额外的局部变量可能会使编译器放弃并生成未优化的代码。然而,这是一个非常罕见的边缘案例;它可以在代码的任何更改时触发,而不仅仅是关于局部变量。
一般来说,编译器优化很难推理,除了众所周知的问题(别名、循环携带的依赖项等)。您可能会觉得自己找到了一些相关的考虑因素,但当您升级编译器或切换到另一个编译器时,它可能会消失。
对随后未修改的局部变量的赋值允许编译器假设该变量中的值不会更改。因此,它可能会决定(例如)将其存储在变量的"使用期限"的寄存器中。这是一个简单的优化,没有自尊的编译器会错过它(除非寄存器压力意味着它被迫溢出)。
这可能会加快代码速度(也可能会稍微减小代码大小)的一个例子是将成员变量分配给局部变量,然后使用该变量而不是成员变量。如果您确信该值不会更改,这可能有助于编译器生成更好的代码。但话说回来,这可能是一个引入bug的好方法,你在玩这样的游戏时必须小心。
正如Thomas Matthews在评论中所说,做你可能认为是多余的任务的另一个好处是帮助调试。它允许在调试运行期间检查(也许还可以调整)变量,这非常方便。我不骄傲,我会犯错,所以我经常这样做。
只有我0.02美元的
温度变量影响优化是不寻常的;通常它们被优化掉了,或者它们帮助编译器一次性加载或计算,而不是重复它(公共子表达式消除)。
如果编译器不能证明对同一类型的其他指针的其他赋值都不能修改该数组元素,那么对arr[i]
的重复访问实际上可能会加载多次。float *__restrict arr
可以帮助编译器找出它,或者float ai = arr[i];
可以告诉编译器读取一次并保持使用相同的值,而不管其他存储。
当然,如果禁用优化,则更多的语句通常比使用更少的大型表达式慢,存储/重新加载延迟瓶颈通常是主要瓶颈。请参阅如何优化这些循环(禁用编译器优化)。但是-O0
(无优化)被认为是缓慢的。如果您的编译至少没有-O2
,最好是-O3 -march=native -ffast-math -flto
,那就是您的问题。
我认为,这给了编译器更多的自由来交错计算表达式,而赋值则迫使编译器插入同步点。
事实上是这样的假设吗?
"同步点";不是正确的技术术语,但FP数学的ISO C++规则确实区分了一个表达式内的优化与跨语句/表达式的优化。
-
仅允许在一个表达式中(如果有的话)将
a * b + c
压缩为fma(a,b,c)
。GCC默认为
-ffp-contract=fast
,允许跨表达式使用。clang默认为strict
或no
,但支持-ffp-contract=fast
。请参阅如何将融合乘加(FMA)指令与SSE/AVX一起使用。如果fast
使带有temp变量的代码运行得和不带temp变量一样快,那么严格的FP收缩规则就是使用temp变量时速度较慢的原因。 -
(传统x87 80位FP数学,或FLT_EVAL_METHOD!=0的其他不寻常机器-FP数学的精度更高,四舍五入到
float
或double
需要额外费用)。严格的ISO C++语义要求在表达式边界处取整,例如赋值。GCC默认忽略-fno-float-store
。但是-std=c++11
或其他什么(而不是-std=gnu++11
)将强制执行额外的取整工作(存储/重新加载会消耗吞吐量和延迟)。这对于使用SSE2的x86标量数学来说不是问题;根据数据类型,计算发生在
float
或double
,使用类似mulsd
(标量双)或mulss
(标量单)的指令。因此它实现了FLT_EVAL_METHOD==0
而不是x87的2
。希望在2023年,没有人会为32位x87构建数字运算代码并关心性能,尤其是在没有提及模糊构建选择的情况下。我提到这一点主要是为了完整性。