avr-gcc:(看似)简单功能中不需要的序言/尾声

Posted

技术标签:

【中文标题】avr-gcc:(看似)简单功能中不需要的序言/尾声【英文标题】:avr-gcc: (seemingly) unneeded prologue/epilogue in simple function 【发布时间】:2019-09-04 01:09:00 【问题描述】:

当尝试处理 uint64 中的单个字节时,AVR gcc⁽¹⁾ 给了我一个奇怪的序言/尾声,而使用 uint32_t 编写的相同函数给了我一个 ret(示例函数是没有)。

为什么 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

【问题讨论】:

你的问题在哪里? @DavidGrayson 我现在添加了它。 看起来像一些参数传递开销,因为 32 位 int 在寄存器中传递,但没有 64 位寄存器。但我不能肯定。 这些函数被很好地优化掉了,因为它们没有被使用。通过堆栈仅返回 64 位值,因此第二个函数在堆栈上分配 8 个字节。如果要删除此功能,请删除此功能。去掉优化选项就可以看到函数的完整实现了。 很难知道你在找什么。您的问题的答案很简单:(1)gcc 这样做是因为它的优化器不够强大,无法将f_u64() 减少为 NOP,并且(2)您可以通过删除函数来删除它或尝试将其实现为 return x;。如果这些不是您要寻找的答案,也许您可​​以改写问题或在评论中详细说明? 【参考方案1】:

我不确定这是否是一个好的答案,但这是我能给出的最好的答案。 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个字节可以被保留。

因此,我最好的选择是这是优化器中的限制或错误(无论您喜欢如何称呼它)。在这种情况下,唯一的“解决方案”是尝试改组实际代码以找到解决方法或将其作为编译器上的错误引发。

【讨论】:

也许编译器无法优化它的原因是序言/尾声中存在 CLI 指令? 这很可能是阻碍它的原因。我实际上对这些陈述的顺序感到困惑。在更新SP(堆栈指针)时禁用中断似乎是合理的,但是在完成SP的更新之前恢复SREG(包括中断启用/禁用位的状态寄存器)就很奇怪了。跨度> 这实际上是一种优化。与延迟槽类似,启用中断的新 SREG 直到下一条指令之后才会生效。 ATtiny48 数据表将此称为“当使用 SEI 指令启用中断时,SEI 之后的指令将在任何未决中断之前执行,如本例所示。” @YannVernier 使用out 更新 SREG 大概也是如此。感谢您澄清这一点。 在AVR-libc FAQ: Why are interrupts re-enabled in the middle of writing the stack pointer?中找到了这种延迟中断处理行为的另一种描述【参考方案2】:

您问如何删除低效的代码。我对你的问题的回答是,你可以摆脱你的函数,因为它不执行任何计算,只是返回传递给它的相同值。

如果您出于某种原因仍希望能够在其他代码中调用该函数,我会这样做:

#define f_u64(x) ((uint64_t)(x))

【讨论】:

这不是我要编写的实际函数,这是编译器行为的一个最小示例。 如果你不展示你的真实代码,那么很难回答你关于如何让行为消失的问题。【参考方案3】:

您看到的开销是 CPU 如何存储数字的Endianness 的结果。在您在编译器资源管理器上引用的示例中,您选择了 Uno - GCC 代码为 ATmega328P (小端)生成 ASM。您还将 uint64 映射到 8 x uint8,因此编译器需要将 64 位数字的高位和低位 32 位部分转过来……然后在返回时将它们转回。 (你会看到 Godbolt 用不同的颜色显示了两个部分。)

如何删除它?这就是 ATmega328P 的工作方式。如果您在 Godbolt 上选择 Raspbain 编译器,您会看到开销消失了 - 因为该平台的字节序是大字节序。

【讨论】:

您能解释一下生成的代码如何以与在堆栈上分配 72 个字节一致的方式操作堆栈指针与字节序转换有关吗?另外,为什么uint64_t 需要字节序转换,而不是uint32_t?对不起,我完全不明白这个答案。

以上是关于avr-gcc:(看似)简单功能中不需要的序言/尾声的主要内容,如果未能解决你的问题,请参考以下文章

使用 AVR Studio 中的自动完成功能使用 avr-gcc 对 C 进行编码

序言问题中不允许内容

如何在 avr-gcc 中定义定时器

致命错误:1:1:序言中不允许内容

序言中不允许内容,但序言在运行时可以吗? Groovy,Jenkins,Java,管道,XML [重复]

Java“序言中不允许的内容。”