为指针运算生成的程序集

Generated Assembly For Pointer Arithmetic

这是一个简单的问题,但我刚遇到。在下面的代码片段中,我创建了三个指针。我知道这三个会表现出相同的行为(都指向同一件事),但老实说我认为代码中的第三个动作是最“有效”的,这意味着它会生成更少的汇编指令来完成与另一个相同的事情二.

我假设前两个必须首先 取消引用一个指针,然后获取被取消引用的内存地址,然后设置一些等于该内存的指针地址。我想到的第三个,只需要将内存地址增加 1.

令我惊讶的是,即使关闭优化,这三个指令都生成相同的汇编指令:https://godbolt.org/z/Weefn4

我是不是遗漏了什么明显的东西?是否有一些编译器魔术可以简单地将这三个识别为等价物?

#include "stdio.h"
#include "stdint.h"

int main()
{
    unsigned int x[10];

    unsigned int* a = &x[1]; // Get address of dereferenced x[1]
    unsigned int* b = &(*(x+1)); // Get address of dereferenced *(x+1)
    unsigned int* c = x+1; // Get address x+1

    printf("%x\n", a);
    printf("%x\n", b);
    printf("%x\n", c);

}

注意 gcc -O0 实际上只禁用跨语句的优化,并且只禁用一些语句内的优化。参见 Disable all optimization options in GCC

在单个语句中,它仍然会在语句中进行一些通常的优化,包括除以非 2 的幂常数的乘法逆元。

一些其他编译器在禁用优化的情况下将 C 语言转写为 asm MSVC 有时会将常量放入寄存器并将其与另一个具有两个立即数的常量进行比较。 GCC 从不做任何愚蠢的事情;它尽可能评估常量表达式并删除始终为假的分支。

如果您想要一个非常注重文字的编译器,请查看 TinyCC,一个一次性编译器。


这种情况下:ISO C标准根据x+1

定义了所有这些

x[y]*(x+y)的语法糖,所以ISO C只需要定义指针数学的规则;指针和整数类型之间的 + 运算符。 + 是可交换的(x+yy+x 完全等价),所以它的变化归结为同一件事也就不足为奇了。在您的情况下,T x[10] 衰减为指针数学的 T*

&*x“取消”:ISO C 抽象机从未真正引用 *x 对象,因此即使 x 是 NULL 指针或指向数组的末尾或其他什么。这就是为什么这需要数组元素的地址,而不是某个临时 *x 对象的地址。 所以这是编译器在执行 code-gen 之前需要解决的事情,而不仅仅是用 mov 负载评估 *x。因为那又怎样?将值保存在寄存器中并不能帮助您获取原始位置的地址。


没有人期望 -O0 (part of the goal is to compile fast, as well as consistent debugging) 提供真正高效的代码,但无偿的随机额外指令即使在不危险的情况下也是不受欢迎的。

GCC 实际上通过程序逻辑的 GIMPLE 和 RTL 内部表示来转换源代码。可能是在那些过程中,表达相同逻辑的不同 C 方式趋于相同。

也就是说,gcc 执行 lea rax, [rbp-80] / add rax, 4 而不是将 + 1*sizeof(unsigned) 折叠到 LEA 中,这有点令人惊讶。如果您使用优化,它当然会这样做。 (并且 volatile unsigned int* 强制它仍然具体化未使用的变量,如果你想让它在没有 printf 调用的代码膨胀的情况下工作。)


其他编译器:

MSVC 有一些区别:https://godbolt.org/z/xoMfT4

;; x86-64 MSVC
        sub     rsp, 88                ; Windows x64 doesn't have a red zone
...
//   unsigned int* a = &x[1]; // Get address of dereferenced x[1]
        mov     eax, 4                          ; even dumber than GCC
        imul    rax, rax, 1                     ; sizeof(unsigned) * 1  I guess?
        lea     rax, QWORD PTR x$[rsp+rax]
        mov     QWORD PTR a$[rsp], rax
//   unsigned int* b = &(*(x+1)); // Get address of dereferenced *(x+1)
        lea     rax, QWORD PTR x$[rsp+4]         ; smarter than GCC
        mov     QWORD PTR b$[rsp], rax
//   unsigned int* c = x+1; // Get address x+1
        lea     rax, QWORD PTR x$[rsp+4]
        mov     QWORD PTR c$[rsp], rax
...

c$[rsp] 只是 [16 + rsp],考虑到它之前定义的 c$ = 16 assemble-时间常数。

ICC 和 clang 以相同的方式编译所有版本。

AArch64 的 MSVC 避免了乘法(并使用十六进制文字而不是十进制)。 但是和x86-64 GCC一样,它把数组基地址放到一个寄存器中,然后加4。 https://godbolt.org/z/ThPxx9

@@ AArch64 MSVC
...
        sub         sp,sp,#0x40
...
//   unsigned int* a = &x[1]; // Get address of dereferenced x[1]
        add         x8,sp,#0x20
        add         x8,x8,#4
        str         x8,[sp]
//    unsigned int* b = &(*(x+1)); // Get address of dereferenced *(x+1)
        add         x8,sp,#0x20
        add         x8,x8,#4
        str         x8,[sp,#8]
//    unsigned int* c = x+1; // Get address x+1
        add         x8,sp,#0x20
        add         x8,x8,#4
        str         x8,[sp,#0x10]
//    unsigned int* d = &1[x];
        add         x8,sp,#0x20
        add         x8,x8,#4
        str         x8,[sp,#0x18]

Clang 使用有趣的策略,将数组基地址一次放入寄存器,然后为每个语句添加。我猜它认为 x86-64 lea 或 AArch64 add x9, sp, #36 是其序言的一部分,如果它想支持在源代码行之间使用 jump 的调试器,如果它可能不会这样做函数中有任何非线性控制流吗?

这三个都被标准定义为等效的:

  • 它明确声明 &*(X) 在所有情况下都与 (X) 完全相同
  • A[B] 定义为 *(A+B).

将第二条规则与第一条规则结合起来,我们得到 &(A[B])(A+B) 相同。


通常,您会注意到还会发生许多其他“优化”。

C 是根据抽象机的输出定义的。所有产生相同输出的程序在标准看来都是等价的程序。

编译器提供的不同优化级别满足可调试性和编译 size/speed 考虑,它们不是语言或任何东西的某些固有级别。