Atmel studio(GCC)即使在空的ISR函数中也会使用大量指令?这可以优化吗



ISR需要很长时间,所以我查看了asm,看看它在做什么。

我用gcc -O3 -mmcu=attiny13a和一些其他选项编译了这个C。

#include <avr/interrupt.h>
ISR(TIM0_COMPA_vect)
{
}

avr-objdump.exe -d test.elf输出:

00000048 <__vector_6>:
48:   1f 92           push    r1
4a:   0f 92           push    r0
4c:   0f b6           in  r0, 0x3f    ; 63
4e:   0f 92           push    r0
50:   11 24           eor r1, r1
52:   0f 90           pop r0
54:   0f be           out 0x3f, r0    ; 63
56:   0f 90           pop r0
58:   1f 90           pop r1
5a:   18 95           reti

虽然C代码是空的,但汇编程序代码是对的吗?

这些链接解释了一些关于ISR()的内容,但没有详细说明需要asm的哪些部分,也没有详细说明是否可以让GCC优化掉简单ISR中不需要它们的一些指令。

  • https://www.nongnu.org/avr-libc/user-manual/group__avr__interrupts.htmlISR()
  • https://gcc.gnu.org/onlinedocs/gcc/AVR-Function-Attributes.html关于CCD_ 5的一些细节

GCC的asm输出(https://godbolt.org/z/zzbY5KE3c)使用类似CCD_ 6的伪指令。

更新的GCC(Godbolt上的9.2)支持-mno-gas-isr-prologues,以使GCC显示与上面Atmel Studio中的反汇编相匹配的真实指令。所以,如果有人想玩这个https://godbolt.org/z/q6M518qfP在真正的Atmel工作室中可能会有同样的效果。

尽管C代码为空,但汇编程序代码是否正确?

是。这是avr gcc v7及以下版本的代码。较新版本的编译器可能会生成更高效的代码,请参阅GCC v8发行说明。原因是:

avr-gcc ABI:R0和R1建模、使用和优点

在设计avr-gcc-ABI时,决定将R0和R1建模为固定寄存器";固定寄存器";意味着编译器不会在寄存器分配或以任何方式使用它们。这些寄存器的唯一用途是在编译的最后阶段,当汇编代码打印到*.s时,这些寄存器可以在各自的输出字符串中隐式使用。这基本上与通过内联汇编输出的指令相同,后者对编译器来说是不透明的。

这一选择背后的原因是,通过手头有这些额外的寄存器,可以提高整体代码质量,其中R0被用作临时寄存器。9和R1 aka。__zero_reg__包含零值。例如,要将寄存器%0中的16位整数与42进行比较,您可以只使用

cpi %A0, 42
cpc %B0, __zero_reg__

没有任何进一步的麻烦,即不需要分配一些临时寄存器,清除它等

R0和R1是固定寄存器的缺点

这种方法的缺点是这些寄存器没有使用寿命信息,例如在之类的乘法代码中

char mul (char x)
{
return x * x * x * x;
}

您必须根据ABI将R1重置为0,因为MUL会破坏其内容:

mul:
mul r24,r24
mov r24,r0
clr r1      ; Superfluous
mul r24,r24 ; Overrides r1
mov r24,r0
clr r1      ; Restore __zero_reg__ to 0
ret

第一个clr r1是多余的,因为后面的mul将覆盖它

ABI的设计也导致了这些昂贵的ISR pro和Epilogue,因为没有关于R0、R1是否被使用或更改的分析,SREG也是如此。因此,的经典ISR序言

  • 保存R0、R1和SREG
  • 将R1设置为0(因为它可能会像上面的mul序列一样暂时保持非0值,但ISR代码要求R1=0)

无论ISR的主体是什么,结语都必须还原它们。

avr gcc v8+解决方案:isr中的伪指令__gcc_isr

由于问题的复杂性,从提交PR20296到解决问题花了12年时间。通过伪指令__gcc_isr将大部分分析从编译器转移到汇编程序。要了解它是如何工作的,请考虑以下C代码:

volatile char c;
__attribute__((__signal__))
void __vector_X (void)
{
++c;
}

以及来自avr gcc v8+-Os -save-temps:的汇编代码

__vector_X:
__gcc_isr 1
lds  r24,c
subi r24,lo8(-1)
sts  c,r24
__gcc_isr 2
reti
__gcc_isr 0,r24

编译器的作用:

  • 如果Binutils不支持__gcc_isr(在配置气体是否接受-mgcc-isr时确定),如果优化关闭,如果ISR被归因于no_gccisr,如果-mgas-isr-prologues已关闭等,则不生成CCD_16。

  • 如果ISR有开放的编码调用或做一些奇怪的事情,如非本地goto(setjmp/longjmp),则不要生成__gcc_isr

  • 如果一切顺利,请打印__gcc_isr伪指令,而不是实际的ISR序言/尾声。

汇编程序的作用:

  • 它分析了从序言块__gcc_isr 1开始到最终块0的occomplete ISR代码,并记录了R0、R1的使用情况以及对SREG的影响。

  • 不要分析函数调用背后的代码:如果遇到[r]call,假设R0、R1和SREG最坏。编译器已经处理了尾部调用(通过某些跳转指令进行的调用)。

  • 根据R0、R1、SREG的使用情况,打印区块1的优化序言和区块2的尾声。用块0指定的寄存器可以用于推送/弹出SREG,因为编译器无论如何都会使用此寄存器。

对于上面的例子,最终代码将是:

<__vector_X>:
8f 93           push r24
8f b7           in   r24, 0x3f  ; SREG
8f 93           push r24
80 91 60 00     lds  r24, 0x0060    ; <c>
8f 5f           subi r24, 0xFF
80 93 60 00     sts  0x0060, r24    ;  <c>
8f 91           pop r24
8f bf           out 0x3f, r24   ; SREG
8f 91           pop r24
18 95           reti

让汇编程序进行分析的明显优点是,它甚至适用于对GCC不透明的内联汇编中的代码。


";内联asm重要吗":关于内联装配的一个注记

首先要注意的是,我们使用当前的方法免费获得对内联asm的分析。内联asm的处理并不是决定让gas来做这项工作的原因,不过这只是一个很好的副作用。所以接下来的基本上是TL;DR为什么我们用汽油当工作马。

内联asm必须明确所有副作用,这是正确的

cc0之前→CC模式转换时,没有条件码寄存器可以对cc0进行缓冲,因此假设基本上每个insn都会对cc0缓冲。随着CC模式的引入,情况并没有发生太大变化(实际上情况变得更糟了):比较insn正在设置CC,但除了分支或超简单的1-指令is之外,几乎所有其他insn都在打击CC。

原因是许多insn都有非常复杂的insn输出打印机,例如用于特定的算术或多字节加载/存储。用任何合理的工作量都不可能在这个级别上对CC的行为进行精确的建模,因此只假设CC会崩溃。这也适用于内联asm:自从CCmode出现以来,avr后端只添加了";cc";clobbers到所有内联程序集,这样遗留代码就不会中断,请参阅avr.cc.

tmp_reg:Insn打印机将隐式使用它,然后和何时使用,因此编译器无法以任何合理的精度计算出它的使用/阻塞状态,即使是,如果它是一个普通的、分配的寄存器而不是固定的寄存器。

zero_reg也是如此,它也是固定的。一些insn打印机只会在特殊情况下使用它,也不可能以合理的方式对此进行建模。正如您已经注意到的,insns(和内联asm)可能假设zero_reg=0,这就是ISR使用单个起作用的原因

asm ("sts 0,__zero_reg__");

将工作并神奇地初始化zero_reg。

当然,在内联asm中添加像"r" (0)这样的隐式操作数是不可能的——即使可能,这也会破坏现有的代码。撞击R0或R1仍然是无效的,因为它们是固定的,所以我们不想依赖撞击者的存在。从技术上讲,一个内联asm会破坏zero_reg,然后将其恢复为0,但不会破坏它。然而,ISR仍然需要知道。

最新更新