我对编译器实际优化的方式没有很多经验,不同级别之间有什么区别(例如-O2与-cc3对于gcc).因此,我不确定以下两个语句对于任意编译器是否相同:
for(i=0;i<10;++i){
variable1*variable2*gridpoint[i];
}
Run Code Online (Sandbox Code Playgroud)
和
variable3=variable1*variable2;
for(i=0;i<10;++i){
variable3*gridpoint[i];
}
Run Code Online (Sandbox Code Playgroud)
从处理时间的角度来看,只计算variable1和variable2的乘积,因为它们在循环中不会改变,这是有意义的.然而,这需要额外的内存,而且我不确定优化器对此开销的影响有多大.如果您从纸张/书籍中获得等式并希望将其转换为计算机可读的内容,则第一个表达式是最容易阅读的.但第二个可能是最快的 - 特别是对于在循环内有很多未变量的更复杂的方程式(我有一些非常令人讨厌的非线性微分方程,我希望在代码中是人类可读的).如果我将变量声明为常量,是否有任何变化?我希望我的问题对任意编译器都有意义,因为我同时使用gcc,Intel和Portland编译器.
小智 4
对于任意编译器来说,很难充分回答这个问题。这段代码能做什么不仅取决于编译器,还取决于目标体系结构。我将尝试解释具有良好功能的生产编译器可以对此代码执行哪些操作。
\n\n\n\n\n从处理时间的角度来看,只计算一次变量 1 和变量 2 的乘积是有意义的,因为它们在循环中不会改变。
\n
你是对的。正如猫先生所指出的,这称为公共子表达式消除。因此,编译器可以生成仅计算表达式一次的代码(或者如果已知两个操作数的值一次是常数,则甚至在编译时计算它)。
\n\n如果一个好的编译器可以确定函数没有副作用,它也可以对函数执行子表达式消除。例如,GCC 可以分析函数体是否可用,但也有pure和const属性可用于专门标记应进行此优化的函数(请参阅函数属性)。
鉴于没有副作用并且编译器能够确定它(在您的示例中,没有任何障碍),其中两个片段在这方面是等效的(我已经用 clang 检查过:-))。
\n\n\n\n\n然而,这需要额外的内存,并且我不确定优化器在多大程度上考虑了这种开销。
\n
事实上,这不需要任何额外的内存。乘法在处理器寄存器中完成,结果也存储在寄存器中。这是消除大量代码并使用单个寄存器来存储结果的问题,这总是很棒的(并且在寄存器分配方面肯定会让生活变得更轻松)方面肯定会让生活变得更容易,尤其是在循环中)。因此,如果可以完成此优化,那么无需额外成本即可完成。
\n\n\n\n\n第一个表达式是最容易阅读的。
\n
GCC 和 Clang 都会执行此优化。不过,我不确定其他编译器,所以你必须自己检查。但很难想象有哪个好的编译器不进行子表达式消除。
\n\n\n\n\n如果我将变量声明为常量,这些会发生变化吗?
\n
它可能。这称为常量表达式 \xe2\x80\x94 ,即仅包含常量的表达式。常量表达式可以在编译期间而不是运行时求值。因此,例如,如果您乘以 A、B 和 C,其中 A 和 B 都是常量,则编译器将A*B针对该预先计算的值仅预先计算多个 C 的表达式。即使对于非常量值,编译器也可以执行此操作,如果它们可以在编译时确定其值并确保它不被更改。例如:
$ cat test.c\ninline int foo(int a, int b)\n{\n return a * b;\n}\n\nint main() {\n int a;\n int b;\n a = 1;\n b = 2;\n return foo(a, b);\n}\n$ clang -Wall -pedantic -O4 -o test ./test.c\n$ otool -tv ./test\n./test:\n(__TEXT,__text) section\n_main:\n0000000100000f70 movl $0x00000002,%eax\n0000000100000f75 ret\nRun Code Online (Sandbox Code Playgroud)\n\n对于上述片段,还可以进行其他优化。以下是我想到的一些:
\n\n第一个最明显的是循环展开。由于迭代次数在运行时已知,编译器可能决定展开循环。是否应用此优化取决于体系结构(即某些 CPU 可以“锁定循环”并比其展开版本更快地执行代码,这也使代码通过使用更少的空间更加缓存友好,避免额外的 \xc2\ xb5OP 融合阶段等)。
\n\n第二个优化是使用SIMD指令(SSE、AVX 等),实际上可以将速度提高 50 倍。例如,GCC 就非常擅长(英特尔一定也很擅长,即使不是更好)。我已经验证了以下功能:
\n\nuint8_t dumb_checksum(const uint8_t *p, size_t size)\n{\n uint8_t s = 0;\n size_t i;\n for (i = 0; i < size; ++i)\n s = (uint8_t)(s + p[i]);\n return s;\n}\nRun Code Online (Sandbox Code Playgroud)\n\n... 转换为一个循环,其中每个步骤一次对 16 个值求和(即如 中所示_mm_add_epi8),并使用额外的代码处理对齐和奇数 (<16) 迭代计数。然而,在我最后一次检查时,Clang 完全失败了。因此,即使迭代次数未知,GCC 也可能会以这种方式减少循环。
如果可以的话,我想建议您不要优化代码,除非您发现它是瓶颈。否则,您可能会浪费大量时间进行错误和过早的优化。
\n\n我希望这回答了你的问题。祝你好运!
\n