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 个字节,然后再次释放它们(因为这里涉及到寄存器 r28r29,它们在开始时保存并在结束时恢复) .

如果你尝试在没有优化的情况下编译(我也跳过了 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" 是尝试打乱真正的代码以找到解决方法或将其作为编译器错误引发。