assembly 为什么表达式'10 + 32'不使用堆栈,而表达式'10 + -32'使用堆栈?

mi7gmzs6  于 2022-11-13  发布在  其他
关注(0)|答案(2)|浏览(312)

所以我自学了一本编译器的教科书,我掌握了它的窍门,但是我有一个关于简单的高级表达式编译的问题,比如52 + -10,到x86-64(AT&T)。
请考虑以下表达式:
10 + 32会产生下列组件:

.globl main
main: 
       movq $10, %rax
       addq $32, %rax
       retq

但是下一个表达式52 + -10生成下面的x86-64程序集:

.globl main
main:
      pushq %rbp
      movq %rsp, %rbp 
      subq $16, %rsp
      movq $10, -8(%rbp) 
      negq -8(%rbp)
      movq -8(%rbp), %rax 
      addq $52, %rax
      addq $16, %rsp
      popq %rbp
      retq

我的理解是这样的:要编译一个高级表达式,比如52 + -10,你需要去掉复杂的操作数,所以你需要做中间变量来编译,比如:

tmp_0 = -10
52 + temp_0

所以我猜,不同之处在于,有中间变量参与的事实。在书中有关这个例子(即52 + -10)它说:
在下一个例子中,我们展示了存储中间结果的内存使用,图2.7列出了一个计算52 +-10的x86程序,这个程序使用了一个称为过程调用栈(简称栈)的内存区域。
所以我想知道为什么表达式52 + -10使用堆栈(由于中间变量),为什么10 + 32不使用堆栈。堆栈是内存的一个区域,用于存储中间变量,但我想知道为什么。

  • 谢谢-谢谢
5cg8jx4n

5cg8jx4n1#

现实世界中的编译器并不是那么笨,即使你告诉他们不要优化(比如clang -O0),他们也会在编译时将常量表达式计算为一个整数,因为这比在将程序转换为汇编代码或机器代码的整个过程中携带所有这些运算符的逻辑要容易。
例如,even MSVC (Godbolt)return 52 + -10编译为mov eax,42/ret,这是一个编译器,在调试版本中有时会做一些疯狂的事情,比如将if(true);else编译为寄存器中的1,并将其与自身进行比较或测试。或者至少使用无条件的X1 M7 N1 X。
编译时eval在C语言中有时是必需的,例如static int arr[10 - 1];是法律的的,静态数组的大小必须是编译时常量。因为编译器需要能够做到这一点,所以在简单的情况下总是这样做是有意义的,即使没有启用优化。
gcc -O0的部分目标是快速编译(而不是简单地/字面地/天真地),而不关心生成代码的效率。在解析后立即对整数表达式求值仍然比携带它并随后为它生成机器代码要快。

但是如果你有一个非常幼稚的编译器,它选择了这样一个低效的编译器:

就像 Alexandria 说的,它可能就像C中的-32不是一个常量,它是一元运算符-应用于常量32。(有趣的是,这就是为什么INT_MIN没有被定义为-2147483648,它选择在编译时不进行常量传播以获取负整数常量。
这个愚蠢的编译器不能在一个寄存器中求反,因为它是一个更大的表达式的第一部分。因为它已经首先处理了+的右边操作数,这与前面的例子mov $10, %eax不同; neg %rax ; add $52, %rax是您可以合理预期的。
您引用了书中的一些材料,解释它的工作原理是发明一个临时变量来保存一元-结果,这在内部是有意义的。
但是它会把这个临时变量当作一个存在于源代码中的变量,并且需要有一个内存地址,就像现实世界中的C编译器在-O0中对没有声明为register的变量所做的那样。(* Why does clang produce inefficient asm with -O0 (for this simple floating point sum)? *)(register除了 * 在-O0中没有做任何效率方面的事情,这就是ISO C++17将其从语言中删除的原因。)
因为它是一个局部变量,所以它被存储在堆栈中。当有任何东西使用堆栈时,它将RBP设置为帧指针,并保留堆栈空间,因此它不假设x86-64 System V ABI的红色区域可用(RSP以下128个字节是安全的,不会受到异步乱码的影响),即使这个变量的生存期包含在表达式中。
我猜更复杂的表达式可能包含函数调用,比如-10 + foo(3),所以如果它的目标是在编译器的 * 这 * 部分实现最大程度的简化,那么这样做是有意义的。代价是为编译器的其他同样简单的部分留下更多的工作要做。即,当没有函数调用时,不寻求将临时值保存在寄存器中的优化。拥有如此简单的编译器意味着对于同一个程序,内部数据结构和生成的asm将更大(效率更低)。

zysjyyx4

zysjyyx42#

所以,我忽略了堆栈是存储局部变量的内存区域。对于每个函数调用,都会分配更多的堆栈空间。在我的例子中,这种分配对应于subq $16, %rsp。我们需要分配一个变量,所以它是8个字节,但这个数字必须能被16整除,因此是16。
看一看here,了解基本知识:-)

相关问题