为什么GCC以不同方式处理两个相似的循环?

注意:

如果我理解下面的代码是正确的,它将跳过整个循环,因为当比较unsigned (j)和signed (-1)时,似乎-1将转换为UINT_MAX 。 (像这个问题解释 )


第一个循环:

 unsigned int j = 10; for (; j > -1; --j) { ---> `>` printf("%u", j); } 

第一个循环的汇编代码的一部分:

 movq %rsp, %rbp .cfi_def_cfa_register 6 movl %edi, -20(%rbp) movq %rsi, -32(%rbp) movl $10, -4(%rbp) nop --->**elision** popq %rbp .cfi_def_cfa 7, 8 ret 

第二个循环的第二个循环:

 unsigned int j = 10; for (; j >= -1; --j) { ---> `>=` printf("%u", j); } 

汇编代码的一部分:

 movq %rsp, %rbp .cfi_def_cfa_register 6 subq $32, %rsp movl %edi, -20(%rbp) movq %rsi, -32(%rbp) movl $10, -4(%rbp) jmp .L2 --->** still a loop here ** .L3: movl -4(%rbp), %eax movl %eax, %esi movl $.LC0, %edi movl $0, %eax call printf subl $1, -4(%rbp) .L2: cmpl $-1, -4(%rbp) je .L3 leave .cfi_def_cfa 7, 8 ret 

所以我的问题是

  • 为什么gcc(我使用GCC 🙁 Ubuntu 4.8.2-19ubuntu1)4.8.2)处理类似情况,它优化第一个但不是第二个 ? (*或者我对代码的理解是错误的?)(它与程序集有关吗?)

编辑:您可以访问此站点进行检查。 如果你只使用-S编译器选项 (或没有编译器选项),你将得到与我相同的结果。 (感谢@Raymond Chen提醒)

步骤1:

在网站上方打开并将以下代码复制到Code Eidtor。

  #include  int main (int argc, char *argv[]) { unsigned int j = 10; for (; j > -1; --j) { printf("%u", j); } } 

第2步:

选择g++ 4.8作为编译器。 编译器选项为空。(或-S)

第3步:

你得到了第一种情况。 现在,将j > -1更改为j >= -1 ,您可以看到第二个。

适用的转换在C标准n1570 S6.3.1.3中描述如下:

…如果新类型是无符号的,则通过重复加或减一个可以在新类型中表示的最大值来转换该值,直到该值在新类型的范围内。

因此-1转换为UINT_MAX ,32位算术为0xffffffff。 这是相同的位模式,因此在汇编语言术语中它是一个无操作。

在第一种情况下,编译器可以确定循环退出条件对于循环变量的所有值都是真实的。 不需要进一步分析,并且在适当的优化水平下,应该省略循环。

在第二种情况下,没有可用的这种微不足道的分析。 但是,如果编译器执行数据流分析,它将发现在进入循环之前满足循环退出条件。 在合适的(但可能不同的)优化级别,也可以省略该循环。

所需的分析在每种情况下都是不同的,在第二种情况下更难。 但是,我不打算预测哪些编译器会在哪些情况下执行循环省略。 你必须测试它们才能找到(就像你一样)。

关于术语的说明:当编译器决定完全省略代码时,术语elision是更精确的描述。 当编译器在不同的可能代码生成策略之间做出选择时,可以更好地使用术语优化,可能在速度和空间之间进行选择

可能因为’j> -1’对于j的任何值都不能为真,而如果j == UINT_MAX则’j> = -1’可以为真。 所以有一个微妙的差异会影响优化。 在第一种情况下,条件和环路可以简单地优化掉; 在第二种情况下,需要稍微复杂的分析。

看起来编译器没有尝试考虑先前初始化的j的“已知”值。 它假设j的初始值可以是任何值,它只是独立地解释周期。

在这种情况下,这两个周期甚至都不相似。

第一个循环是一个“不可能”的循环。 它包含迭代条件j > -1 ,它将被解释为j > UINT_MAX 。 显然,这是一个不可能的条件。 因此,编译器决定完全消除循环。

第二个周期的条件并非不可能。 它相当于j >= UINT_MAX并且对于j == UINT_MAX是完全可满足的。 因此,第二个循环直接转换为调用printf的完全正常的代码。

显然,第二个版本中的循环永远不会进行多次迭代,这意味着实际上不需要实际循环。 这不是编译器能够(甚至尝试)自己弄清楚的东西。 你问了一个循环 – 它给你一个循环。