【问题标题】:help understanding differences between #define, const and enum in C and C++ on assembly level帮助理解 C 和 C++ 中#define、const 和 enum 在汇编级别上的区别
【发布时间】:2010-03-08 10:05:21
【问题描述】:

最近,我正在研究#define、const 和 enum 的汇编代码:

C 代码(#define):

3   #define pi 3  
4   int main(void)
5   {
6      int a,r=1;             
7      a=2*pi*r;
8      return 0;
9   }

GCC 生成的汇编代码(c 代码中的第 6 行和第 7 行):

6   mov $0x1, -0x4(%ebp)
7   mov -0x4(%ebp), %edx
7   mov %edx, %eax
7   add %eax, %eax
7   add %edx, %eax
7   add %eax, %eax
7   mov %eax, -0x8(%ebp)

C 代码(枚举):

2   int main(void)
3   {
4      int a,r=1;
5      enum{pi=3};
6      a=2*pi*r;
7      return 0;
8   }

GCC 生成的汇编代码(c 代码中的第 4 行和第 6 行):

6   mov $0x1, -0x4(%ebp)
7   mov -0x4(%ebp), %edx
7   mov %edx, %eax
7   add %eax, %eax
7   add %edx, %eax
7   add %eax, %eax
7   mov %eax, -0x8(%ebp)

C 代码(常量):

4   int main(void)
5   {
6      int a,r=1;  
7      const int pi=3;           
8      a=2*pi*r;
9      return 0;
10  }

GCC 生成的汇编代码(c 代码中的第 7 行和第 8 行):

6   movl $0x3, -0x8(%ebp)
7   movl $0x3, -0x4(%ebp)
8   mov  -0x4(%ebp), %eax
8   add  %eax, %eax
8   imul -0x8(%ebp), %eax
8   mov  %eax, 0xc(%ebp)

我发现使用#defineenum,汇编代码是一样的。编译器使用 3 个加法指令来执行乘法运算。但是,当使用const 时,会使用imul 指令。 有谁知道这背后的原因吗?

【问题讨论】:

  • 什么版本的 gcc 和什么优化标志?
  • gcc (GCC) 4.4.2 20091222 (Red Hat 4.4.2-20) 我没有指定优化标志(默认)。

标签: c gcc assembly x86


【解决方案1】:

不同之处在于#defineenum 的值 3 不需要作为显式值存在于代码中,因此编译器决定使用两个 add 指令而不是为常量 3 分配空间. add reg,reg 指令是每条指令 2 个字节,因此是 6 个字节的指令和 0 个字节用于常量乘以 3,这比 imul 加上 4 个字节的常量更小。加上 add 指令的使用方式,它可以很好地翻译 *2 *3,所以这可能不是大小优化,它可能是当你乘以 2 或乘以 3 时的默认编译器输出。(添加通常是比乘法更快的指令)。

#defineenum 不声明实例,它们仅提供一种为值 3 赋予符号名称的方法,因此编译器可以选择制作更小的代码。

  mov $0x1, -0x4(%ebp)    ; r=1
  mov -0x4(%ebp), %edx    ; edx = r
  mov %edx, %eax          ; eax = edx
  add %eax, %eax          ; *2
  add %edx, %eax          ; 
  add %eax, %eax          ; *3
  mov %eax, -0x8(%ebp)    ; a = eax

但是,当您声明 const int pi = 3 时,您告诉编译器为整数值分配空间并使用 3 对其进行初始化。这使用 4 个字节,但该常量现在可用作 imul 指令的操作数.

 movl $0x3, -0x8(%ebp)     ; pi = 3
 movl $0x3, -0x4(%ebp)     ; r = 3? (typo?)
 mov  -0x4(%ebp), %eax     ; eax = r
 add  %eax, %eax           ; *2
 imul -0x8(%ebp), %eax     ; *pi
 mov  %eax, 0xc(%ebp)      ; a = eax

顺便说一句,这显然不是优化的代码。因为a这个值从来没有被使用过,所以如果开启优化,编译器只会执行

xor eax, eax  ; return 0

在所有 3 种情况下。

附录:

我用 MSVC 尝试过这个,在调试模式下,我得到了所有 3 种情况的相同输出,MSVC 总是通过文字 6 使用 imul。即使在情况 3 中创建 const int = 3 时它实际上并没有在其中引用它仿真器。

我认为这个测试并没有真正告诉你关于 const vs define vs enum 的任何信息,因为这是未优化的代码。

【讨论】:

  • +1;关于最后一段;这就是 C++ 在 const 语义上与 C 不同的地方。除非你获取它的地址,否则 C++ 的行为就好像 pi 是一个文字,所以我应该相信他在 C++ 编译的所有三种情况下都会得到相同的代码。
  • @Clifford:当我在我的编译器上开启完全优化时,程序变成了return 0,因为 a 从未被使用过。
  • 但是为什么add可以用,imul不行呢?我想知道这背后的原因?比如在使用#define & enum 时可以使用哪些指令,哪些指令不能使用,或者这取决于我使用的编译器?如果是真的,这取决于编译器的机制?
  • @martin:我不会太担心。如果你打开优化器,结果会很不一样。
  • 关于a 的分配被优化,声明它volatile,这样可以防止这种情况发生。关于马丁的担忧;使用编译器的全部意义在于不必担心汇编程序——让编译器完成它的工作;-)。
【解决方案2】:

const 关键字仅表明访问它的特定文件不允许修改它,但其他模块可以修改或定义该值。因此,不允许移位和乘法,因为事先不知道该值。 #define'ed 值在预处理后简单地替换为文字值,因此编译器可以在编译时对其进行分析。不过,我并不完全确定枚举。

【讨论】:

  • 您的第一个陈述对于 const 函数参数是正确的,但对于本例中的局部变量则不然;根据定义,外部模块或函数不能修改它。
  • 根据你的解释,使用const的时候,imul指令是不能用的,为什么要用呢?
  • 我应该说移位和,而不是移位和乘法。例如,要乘以 13,您可以将 13 分解为 (x
  • 编译器可以看到pi 的值为3,并且可以合法地利用这一事实。只是不需要这样做。
【解决方案3】:

编译为 C++ 时,生成的代码与使用 C 编译时生成的代码相同,至少使用 GCC 4.4.1:

const int pi = 3;

...

a=2*pi*r;
-   0x40132e    <main+22>:      mov    0xc(%esp),%edx
-   0x401332    <main+26>:      mov    %edx,%eax
-   0x401334    <main+28>:      shl    %eax
-   0x401336    <main+30>:      add    %edx,%eax
-   0x401338    <main+32>:      shl    %eax
-   0x40133a    <main+34>:      mov    %eax,0x8(%esp)

如果将 pi 定义为:

#define pi 3

【讨论】:

  • 我想知道为什么编译器不使用 lea eax,[eax*2+eax] 进行乘以 3 的步骤。
【解决方案4】:

在最后一种情况下,编译器将 pi 视为变量而不是文字常量。编译器可能会使用不同的编译器选项对其进行优化。

[edit]请注意,由于a 已分配但未使用,因此可以优化写入的整个片段;将a 声明为volatile 以防止这种情况发生。

const 在 C++ 中的语义与 C 中的语义略有不同,我怀疑使用 C++ 编译你会得到不同的结果。

【讨论】:

    【解决方案5】:

    看来您实际上并没有打开优化器——即使您认为已经打开了。我编译了这个:

    int literal(int r)
    {
      return 2*3*r;
    }
    int enumeral(int r)
    {
      enum { pi=3 };
      return 2*pi*r;
    }
    int constant(int r)
    {
      const int pi=3;
      return 2*pi*r;
    }
    

    ... 使用 Apple 的 gcc 4.2(而不是您使用的编译器);当我不使用优化(默认)时,我可以重现您所说的程序集,但在任何更高的优化级别,我都会得到所有三个相同的代码,以及编译为 C 或 C++ 的相同代码:

        movl    4(%esp), %eax
        leal    (%eax,%eax,2), %eax
        addl    %eax, %eax
        ret
    

    根据 John Knoeller 的回答,您似乎没有意识到 GCC 的命令行选项区分大小写资本 O options (-O1, -O2, -O3, -Os) 开启优化;小写 o 选项 (-o whatever) 指定输出文件。您的-o2 -othing 构造会默默地忽略-o2 部分并写入thing

    【讨论】:

      猜你喜欢
      • 2020-03-03
      • 1970-01-01
      • 1970-01-01
      • 2011-06-13
      • 1970-01-01
      • 1970-01-01
      • 2018-07-07
      • 1970-01-01
      • 2011-09-20
      相关资源
      最近更新 更多