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

时间:2014-08-02 13:50:26

标签: c loops gcc

注意:

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


第一个循环:

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)处理类似的情况,优化第一个但不是第二个? (*或者我对代码的理解是错误的?)(它与程序集有关?)

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

第1步:

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

 #include <stdio.h>
 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,您可以看到第二个。

3 个答案:

答案 0 :(得分:4)

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

  

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

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

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

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

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

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

答案 1 :(得分:2)

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

答案 2 :(得分:1)

看起来编译器没有尝试考虑&#34;已知&#34;来自先前初始化的j的值。在假设j的初始值可以是任何值的情况下,它只是独立地解释周期。

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

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

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

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