avr-gcc:(看似)在简单功能中不需要 prologue/epilogue
avr-gcc: (seemingly) unneeded prologue/epilogue in simple function
当试图处理 uint64
中的单个字节时,AVR gcc⁽¹⁾ 给了我一个奇怪的 prologue/epilogue,而使用 uint32_t
编写的相同函数给了我一个 ret
(示例函数是 NOP)。
gcc 为什么要这样做?我该如何删除它?
You can see the code here, in Compiler Explorer.
⁽¹⁾ gcc 5.4.0 来自 Arduino 1.8.9 发行版,参数=-O3 -std=c++11
.
源代码:
#include <stdint.h>
uint32_t f_u32(uint32_t x) {
union y {
uint8_t p[4];
uint32_t w;
};
return y{ .p = {
y{ .w = x }.p[0],
y{ .w = x }.p[1],
y{ .w = x }.p[2],
y{ .w = x }.p[3]
} }.w;
}
uint64_t f_u64(uint64_t x) {
union y {
uint8_t p[8];
uint64_t w;
};
return y{ .p = {
y{ .w = x }.p[0],
y{ .w = x }.p[1],
y{ .w = x }.p[2],
y{ .w = x }.p[3],
y{ .w = x }.p[4],
y{ .w = x }.p[5],
y{ .w = x }.p[6],
y{ .w = x }.p[7]
} }.w;
}
为 uint32_t
版本生成的程序集:
f_u32(unsigned long):
ret
为 uint64_t
版本生成的程序集:
f_u64(unsigned long long):
push r28
push r29
in r28,__SP_L__
in r29,__SP_H__
subi r28,72
sbc r29,__zero_reg__
in __tmp_reg__,__SREG__
cli
out __SP_H__,r29
out __SREG__,__tmp_reg__
out __SP_L__,r28
subi r28,-72
sbci r29,-1
in __tmp_reg__,__SREG__
cli
out __SP_H__,r29
out __SREG__,__tmp_reg__
out __SP_L__,r28
pop r29
pop r28
ret
您询问了如何删除低效代码。我对你的问题的回答是,你可以去掉你的函数,因为它不执行任何计算,只是返回传递给它的相同值。
如果出于某种原因您仍然希望能够在其他代码中调用该函数,我会这样做:
#define f_u64(x) ((uint64_t)(x))
您看到的开销是 Endianness of how the CPU stores numbers. In the example you refer to on Compiler Explorer you've selected the Uno - that GCC code generates ASM for the ATmega328P(小端)的结果。您还将 uint64 映射到 8 x uint8,因此编译器需要转动 64 位数字的高阶和低阶 32 位部分......然后将它们转回 return。 (你会看到godbolt以不同的颜色显示了两个部分。)
如何删除它?这就是 ATmega328P 的工作方式。你会看到如果你 select Godbolt 上的 Raspbain 编译器开销消失了 - 因为该平台的 Endianness 是 big-endian。
我不确定这是否是一个好的答案,但这是我能给出的最好答案。 f_u64()
函数的汇编在栈上分配了 72 个字节,然后再次释放它们(因为这里涉及到寄存器 r28
和 r29
,它们在开始时保存并在结束时恢复) .
如果你尝试在没有优化的情况下编译(我也跳过了 c++11
标志,我认为这没有任何区别),那么你会看到 f_u64()
函数从分配 80 开始堆栈上的字节数(类似于您在优化代码中看到的开头语句,只是 80 字节而不是 72 字节):
in r28,__SP_L__
in r29,__SP_H__
subi r28,80
sbc r29,__zero_reg__
in __tmp_reg__,__SREG__
cli
out __SP_H__,r29
out __SREG__,__tmp_reg__
out __SP_L__,r28
这80个字节其实都用完了。首先存储参数 x
的值(8 个字节),然后进行涉及剩余 72 个字节的大量移动数据。
之后这 80 个字节被释放到堆栈上,类似于优化代码中的关闭语句:
subi r28,-80
sbci r29,-1
in __tmp_reg__,__SREG__
cli
out __SP_H__,r29
out __SREG__,__tmp_reg__
out __SP_L__,r28
我的猜测是优化器认为可以节省用于存储参数的 8 个字节。因此它只需要 72 个字节。然后它得出结论,所有数据的移动都可以幸免。然而,它没有弄清楚,这意味着堆栈上的72个字节可以被节省下来。
因此,我最好的选择是这是优化器中的限制或错误(无论您喜欢如何称呼它)。在那种情况下,唯一的 "solution" 是尝试打乱真正的代码以找到解决方法或将其作为编译器错误引发。
当试图处理 uint64
中的单个字节时,AVR gcc⁽¹⁾ 给了我一个奇怪的 prologue/epilogue,而使用 uint32_t
编写的相同函数给了我一个 ret
(示例函数是 NOP)。
gcc 为什么要这样做?我该如何删除它?
You can see the code here, in Compiler Explorer.
⁽¹⁾ gcc 5.4.0 来自 Arduino 1.8.9 发行版,参数=-O3 -std=c++11
.
源代码:
#include <stdint.h>
uint32_t f_u32(uint32_t x) {
union y {
uint8_t p[4];
uint32_t w;
};
return y{ .p = {
y{ .w = x }.p[0],
y{ .w = x }.p[1],
y{ .w = x }.p[2],
y{ .w = x }.p[3]
} }.w;
}
uint64_t f_u64(uint64_t x) {
union y {
uint8_t p[8];
uint64_t w;
};
return y{ .p = {
y{ .w = x }.p[0],
y{ .w = x }.p[1],
y{ .w = x }.p[2],
y{ .w = x }.p[3],
y{ .w = x }.p[4],
y{ .w = x }.p[5],
y{ .w = x }.p[6],
y{ .w = x }.p[7]
} }.w;
}
为 uint32_t
版本生成的程序集:
f_u32(unsigned long):
ret
为 uint64_t
版本生成的程序集:
f_u64(unsigned long long):
push r28
push r29
in r28,__SP_L__
in r29,__SP_H__
subi r28,72
sbc r29,__zero_reg__
in __tmp_reg__,__SREG__
cli
out __SP_H__,r29
out __SREG__,__tmp_reg__
out __SP_L__,r28
subi r28,-72
sbci r29,-1
in __tmp_reg__,__SREG__
cli
out __SP_H__,r29
out __SREG__,__tmp_reg__
out __SP_L__,r28
pop r29
pop r28
ret
您询问了如何删除低效代码。我对你的问题的回答是,你可以去掉你的函数,因为它不执行任何计算,只是返回传递给它的相同值。
如果出于某种原因您仍然希望能够在其他代码中调用该函数,我会这样做:
#define f_u64(x) ((uint64_t)(x))
您看到的开销是 Endianness of how the CPU stores numbers. In the example you refer to on Compiler Explorer you've selected the Uno - that GCC code generates ASM for the ATmega328P(小端)的结果。您还将 uint64 映射到 8 x uint8,因此编译器需要转动 64 位数字的高阶和低阶 32 位部分......然后将它们转回 return。 (你会看到godbolt以不同的颜色显示了两个部分。)
如何删除它?这就是 ATmega328P 的工作方式。你会看到如果你 select Godbolt 上的 Raspbain 编译器开销消失了 - 因为该平台的 Endianness 是 big-endian。
我不确定这是否是一个好的答案,但这是我能给出的最好答案。 f_u64()
函数的汇编在栈上分配了 72 个字节,然后再次释放它们(因为这里涉及到寄存器 r28
和 r29
,它们在开始时保存并在结束时恢复) .
如果你尝试在没有优化的情况下编译(我也跳过了 c++11
标志,我认为这没有任何区别),那么你会看到 f_u64()
函数从分配 80 开始堆栈上的字节数(类似于您在优化代码中看到的开头语句,只是 80 字节而不是 72 字节):
in r28,__SP_L__
in r29,__SP_H__
subi r28,80
sbc r29,__zero_reg__
in __tmp_reg__,__SREG__
cli
out __SP_H__,r29
out __SREG__,__tmp_reg__
out __SP_L__,r28
这80个字节其实都用完了。首先存储参数 x
的值(8 个字节),然后进行涉及剩余 72 个字节的大量移动数据。
之后这 80 个字节被释放到堆栈上,类似于优化代码中的关闭语句:
subi r28,-80
sbci r29,-1
in __tmp_reg__,__SREG__
cli
out __SP_H__,r29
out __SREG__,__tmp_reg__
out __SP_L__,r28
我的猜测是优化器认为可以节省用于存储参数的 8 个字节。因此它只需要 72 个字节。然后它得出结论,所有数据的移动都可以幸免。然而,它没有弄清楚,这意味着堆栈上的72个字节可以被节省下来。
因此,我最好的选择是这是优化器中的限制或错误(无论您喜欢如何称呼它)。在那种情况下,唯一的 "solution" 是尝试打乱真正的代码以找到解决方法或将其作为编译器错误引发。