这是一个简单的问题,但我刚刚遇到它。在下面的代码片段中,我创建了三个指针。我知道这三个动作会表现出等效的行为(都指向同一件事),但老实说,我认为代码中的第三个动作是最"有效"的,这意味着它会生成更少的汇编指令来完成与其他两个相同的事情。
我假设前两个必须首先尊重一个指针,然后获取被取消引用的事物的内存地址,然后设置一些指针等于该内存地址。第三个我想,只需要将内存地址增加 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+y
和y+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 是根据抽象机器的输出定义的。 在标准眼中,所有产生相同输出的程序都是等效的程序。
编译器提供的不同优化级别迎合了可调试性和编译大小/速度的考虑,它们不是语言的某些固有级别或任何东西。