这个问题很难为任意编译器做出充分的回答。对于这段代码来说,能够做什么不仅取决于编译器,还取决于目标架构。我将尝试解释一下一个具有良好功能的生产编译器可以如何处理这段代码。
从处理时间的角度来看,只计算variable1和variable2的乘积一次是有意义的,因为它们在循环中不会改变。
你是正确的。正如Cat先生所指出的那样,这被称为
公共子表达式消除。因此,编译器可以生成代码仅计算一次表达式(甚至在编译时计算它,如果两个操作数的值在某个时刻已知为常量)。
如果编译器能够确定函数没有副作用,一个体面的编译器也可以对函数执行子表达式消除。例如,GCC可以分析一个函数,如果其主体可用,但也有
pure
和
const
属性可用于特别标记应该受到此优化的函数(请参阅
函数属性)。
鉴于没有副作用并且编译器能够确定它(在您的示例中,没有任何障碍),这方面的两个代码片段是等效的(我已经用clang进行了检查:-)。
然而,这需要额外的内存,我不确定优化器如何强烈地考虑这种开销。
实际上,这不需要任何额外的内存。乘法是在处理器寄存器中完成的,并且结果也存储在寄存器中。这是消除大量代码并使用单个寄存器存储结果的问题,这总是很好的(并且在寄存器分配方面肯定会使生活更轻松,特别是在循环中)。因此,如果可以进行此优化,则将不会产生额外的成本。
第一个表达式最容易阅读..
无论如何GCC和Clang都将执行此优化。但是我不确定其他编译器,所以您需要自己检查。但是很难想象没有进行子表达式消除的好编译器。
如果我将变量声明为常量,这会改变什么吗?
可能会有所不同。这被称为常量表达式,即只包含常量的表达式。常量表达式可以在编译期间而不是运行时进行评估。因此,例如,如果你将A和B都定义为常量并将它们相乘,编译器将仅预先计算A*B
表达式,然后将C与该预先计算的值相乘。编译器甚至可以对非常量值进行此操作,如果它们可以确定其值在编译时且确定其不会更改。例如:
$ cat test.c
inline int foo(int a, int b)
{
return a * b;
}
int main() {
int a;
int b;
a = 1;
b = 2;
return foo(a, b);
}
$ clang -Wall -pedantic -O4 -o test ./test.c
$ otool -tv ./test
./test:
(__TEXT,__text) section
_main:
0000000100000f70 movl $0x00000002,%eax
0000000100000f75 ret
除了上述代码片段,还有其他可以进行优化的地方。以下是一些我想到的:
第一个也是最明显的是循环展开。由于迭代次数在运行时已知,编译器可以决定展开循环。是否应用此优化取决于架构(即某些CPU可以“锁定您的循环”并比其展开版本更快地执行代码,这也通过使用更少的空间、避免额外的µOP融合阶段等使代码更加缓存友好)。
第二个优化可以将速度提高50倍,即使用 SIMD指令(SSE、AVX等)。例如,GCC非常擅长这方面的优化(如果不是更好的话,英特尔也必须如此)。我已经验证了以下功能:
uint8_t dumb_checksum(const uint8_t *p, size_t size)
{
uint8_t s = 0;
size_t i;
for (i = 0; i < size; ++i)
s = (uint8_t)(s + p[i]);
return s;
}
...被转化成一个循环,每一步都会同时累加16个值(例如,像_mm_add_epi8
一样),并且还有额外的代码处理对齐和奇数(<16)迭代计数。然而,最近一次检查时,Clang失败了。因此,即使迭代次数未知,GCC也可能以这种方式缩小您的循环。
如果我可以的话,我建议您不要优化代码,除非您发现它是瓶颈。否则,您可能会浪费大量时间进行虚假和过早的优化。
希望这回答了您的问题。祝你好运!
volatile
变量是与内存空间的一个区域相关联的变量,它可以在代码正常流程之外发生变化...例如,映射到微处理器的输入寄存器的变量,或者映射到时钟寄存器的变量。因此,每次访问它时,它可能已经改变,即使没有代码分配给它一个值。因此,编译器不会将其提取到循环的外部。 - Dancrumb