我已经将简单定义与计算定义进行了比较,汇编代码使我感到惊讶。
#define HOURS 365*24
和
#define HOURS 8760
然后主要是int a = HOURS
。
它们两者都生成看起来完全相同的汇编代码。这在gcc(版本7.3.0,64位体系结构)和avr-gcc(版本5.4.0,8位体系结构)中都适用。为什么会这样?据我了解,define只是一个预处理器,它将在编译之前替换文件中的确切字符串。如果是这样,为什么在运行时不进行计算?看到常量,这是gcc的把戏吗?
答案 0 :(得分:3)
任何好的编译器都会在编译时评估简单表达式。您看到的行为与预处理无关。如果您直接写:
int a = 365 * 24;
然后,任何好的编译器都会生成将a
初始化为8760的代码。(某些编译器如果在禁用优化功能的情况下执行,则可能不会。但是,当启用优化时,任何体面的编译器都会执行此优化。)< / p>
在现代编译器中,优化的作用远不止于此。例如,它们将在循环外标识不变代码。 (也就是说,如果循环内有表达式或什至是完整语句,其结果不取决于当前循环迭代中的数据,则它们会将这些表达式或语句移到循环外,因此它们只被求值一次循环执行的时间间隔。)它们将删除从未使用过的对象。如果条件分支的一个路径在某些情况下具有未定义的行为,则它们可能会推断出该路径永远不会被采用,因此他们将在那些情况下通过删除该路径和条件分支来优化代码。他们甚至可以用等效的数学表达式代替整个循环。例如,当我编译时:
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
int n = atoi(argv[1]);
int s = 0;
for (int i = 1; i <= n; ++i)
s += i;
printf("%d\n", s);
}
对于Apple LLVM 9.1.0(clang-902.0.39.2),生成的汇编代码不包含循环。调用atoi
后,它执行实际上是 1 的printf("%d\n", 0 < n ? n*(n+1)/2 : 0);
指令。因此,该循环完全由测试和简单的算术表达式代替。
1 实际说明是:
leal -1(%rax), %ecx
movl %eax, %edx
addl $-2, %edx
imulq %rcx, %rdx
shrq %rdx
leal -1(%rdx,%rax,2), %esi
更接近的表示法是(n-1)*(n-2)/2 + 2*n - 1
。编译器执行此操作的原因之一是,如果n
为INT_MAX
,它将得到“正确”的结果,因为n*(n+1)/2
会产生零,因为n+1
换行,而{{ 1}}得到的结果与循环得到的结果相同(假定为包装算术),由于包装,结果为(n-1)*(n-2)/2 + 2*n - 1
或INT_MAX+1
。根据C规则,编译器可以使用更简单的INT_MIN
,因为溢出的行为不是由C定义的,但是此编译器可能符合更严格的规则,例如使用包装二进制补码算法。