c语言 - 为指针算术生成的程序集



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

我假设前两个必须首先尊重一个指针,然后获取被取消引用的事物的内存地址,然后设置一些指针等于该内存地址。第三个我想,只需要将内存地址增加 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("%xn", a);
printf("%xn", b);
printf("%xn", c);
}

请注意,gcc -O0实际上只禁用跨语句的优化,并且只禁用语句内的某些优化。 请参阅禁用 GCC 中的所有优化选项。

在单个语句中,它仍然在语句内执行一些通常的优化,包括除以非 2 次幂常量的乘法逆。

其他一些编译器在禁用优化的情况下将 C 转换为 asm 进行更多的脑死亡音译,例如 MSVC 有时会将一个常量放入寄存器中,并将其与另一个常量进行比较,并带有两个即时常量。 海湾合作委员会从不做任何愚蠢的事情;它尽可能计算常量表达式并删除总是错误的分支。

如果你想要一个非常字面意思的编译器,看看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对象的地址。因此,这是编译器在执行代码生成之前需要解决的问题,而不仅仅是使用mov负载评估*x。 因为那又怎样? 在寄存器中具有该值并不能帮助您获取原始位置的地址。


没有人期望从-O0获得真正高效的代码(部分目标是快速编译,以及一致的调试),但无端的随机额外指令即使在不危险的情况下也是不受欢迎的。

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汇编时间常数。

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-64lea或 AArch64add x9, sp, #36其序言的一部分,如果它想支持在源代码行之间使用jump的调试器,并且如果函数中有任何非线性控制流,也许不会这样做?

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

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

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


一般来说,你也会注意到一堆其他的"优化"。

C 是根据抽象机器的输出定义的。 在标准眼中,所有产生相同输出的程序都是等效的程序。

编译器提供的不同优化级别迎合了可调试性和编译大小/速度的考虑,它们不是语言的某些固有级别或任何东西。

最新更新