编译器代码生成比较



好,一切都从这里开始:无符号整数和无符号字符保持相同的值,但行为不同,为什么?

我写了下面的应用程序来理解幕后发生了什么(即编译器是如何处理这个问题的)。

#include <stdio.h>
int main()
{
  {
  unsigned char k=-1;
  if(k==-1)
  {
    puts("uc okn");
  }
  }
  {
  unsigned int k=-1;
  if(k==-1)
  {
    puts("ui ok");
  }
  }
}

在使用GCC编译时,如:

gcc -O0 -S -masm=intel h.c 

我得到以下程序集文件:

    .file   "h.c"
    .intel_syntax noprefix
    .section        .rodata
.LC0:
    .string "ui ok"
    .text
    .globl  main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    push    rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    mov     rbp, rsp
    .cfi_def_cfa_register 6
    sub     rsp, 16
    mov     BYTE PTR [rbp-1], -1
    mov     DWORD PTR [rbp-8], -1
    cmp     DWORD PTR [rbp-8], -1
    jne     .L3
    mov     edi, OFFSET FLAT:.LC0
    call    puts
.L3:
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .ident  "GCC: (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3"
    .section        .note.GNU-stack,"",@progbits

令我惊讶的是,第一张支票甚至都不在那里。

但是,如果我用Microsoft Visual c++(2010)编译同样的东西,我得到(我已经从这个清单中删除了很多垃圾,这就是为什么它不是那么有效):

00B81780  push        ebp  
00B81781  mov         ebp,esp  
00B81783  sub         esp,0D8h  
00B81789  push        ebx  
00B8178A  push        esi  
00B8178B  push        edi  
00B8178C  lea         edi,[ebp-0D8h]  
00B81792  mov         ecx,36h  
00B81797  mov         eax,0CCCCCCCCh  
00B8179C  rep stos    dword ptr es:[edi]  
00B8179E  mov         byte ptr [k],0FFh  
00B817A2  movzx       eax,byte ptr [k]  
00B817A6  cmp         eax,0FFFFFFFFh  
00B817A9  jne         wmain+42h (0B817C2h)  
00B817AB  mov         esi,esp  
00B817AD  push        offset string "uc okn" (0B857A8h)  
00B817B2  call        dword ptr [__imp__puts (0B882ACh)]  
00B817B8  add         esp,4  
00B817BB  cmp         esi,esp  
00B817BD  call        @ILT+435(__RTC_CheckEsp) (0B811B8h)  
00B817C2  mov         dword ptr [k],0FFFFFFFFh  
00B817C9  cmp         dword ptr [k],0FFFFFFFFh  
00B817CD  jne         wmain+66h (0B817E6h)  
00B817CF  mov         esi,esp  
00B817D1  push        offset string "ui ok" (0B857A0h)  
00B817D6  call        dword ptr [__imp__puts (0B882ACh)]  
00B817DC  add         esp,4  
00B817DF  cmp         esi,esp  
00B817E1  call        @ILT+435(__RTC_CheckEsp) (0B811B8h)  

问题是:为什么会这样?为什么GCC"跳过"第一个IF,我怎样才能强迫GCC不跳过它?优化是禁用的,但似乎它仍然优化掉了一些东西…

我的猜测(我不是GCC开发人员)是它做了足够的静态分析来证明自己第一个if的测试从来都不是真的。

不应该太困难,因为在初始化和测试之间没有代码,没有任何副作用或外部实体可以改变变量。

只是出于好奇,尝试使变量static和/或volatile,看看是否有任何变化

这看起来像是GCC的一个问题,尽管承认是一个非常小的问题。

来自GCC的文档网站(强调我的):

如果没有任何优化选项,编译器的目标是减少编译成本,并使调试产生预期的结果。语句是独立的:如果您在语句之间使用断点停止程序,那么您可以为任何变量赋一个新值或将程序计数器更改为函数中的任何其他语句,并从源代码中获得您期望的结果。

因此,对于-O0,您应该能够在unsigned char k=-1;if(k==-1)之间放置一个断点,在该断点期间修改k,并期望采取分支;但是对于发出的代码,这是不可能的。

Updated:我的猜测是,char作为基数(int)类型以下的类型,只是为了比较而升级为整数类型。(假设编译器将字面量作为整数,并且通常更喜欢单词大小的整数而不是字节大小的整数)

作为一个无符号值,零扩展总是正的(注意MOVZX而不是有符号的变体!),所以检查可能通过基本常数传播优化了。

你可以尝试强制文本为字节(强制转换或后缀),例如与((unsigned char)(-1))比较,然后编译器可能会插入一个1字节的比较,结果可能会不同。

这里有一些细微之处:

  • 编译器甚至不需要查看k的初始化来证明条件k==-1在unsigned char的情况下永远不可能为真。关键是,unsigned 8位值需要提升为32位,因为比较的右侧是一个整数常量,默认为32位。因为k是无符号的,所以这次提升的结果是00000000 00000000 00000000 xxxxxxxx。常数-1的位模式是11111111 11111111 11111111 11111111,所以不管xxxxxxxx是什么,比较的结果总是假的。
  • 在这一点上我可能是错的,但我相信即使k被指定为volatile,编译器也只需要将其加载到寄存器中(因为加载操作可能会触发硬件中的一些期望的副作用),而不是实际执行比较或为不可达的if块生成代码。
  • 实际上,对于不可达的代码省略生成汇编完全符合- 0的目标,以加快编译过程。
  • 当然,unsigned常量和负常量之间的比较是未定义的行为。至少,根本没有机器指令来正确处理这种情况,编译器不会插入必要的代码来处理它,正如您从反汇编中看到的那样。你得到的只是有符号和无符号之间的隐式强制转换,导致整数溢出(这本身就是未定义的行为),以及未混合符号的比较。

最新更新