Atmel studio(GCC)即使在空ISR函数中也使用了大量指令?是否可以优化?

apeeds0o  于 2022-11-12  发布在  其他
关注(0)|答案(1)|浏览(185)

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中不需要的一些指令。

  • 宏指令
  • 关于__attribute__((interrupt))的一些细节。

GCC的asm输出(https://godbolt.org/z/zzbY5KE3c)使用了__gcc_isr 1之类的伪指令。
更新的GCC(Godbolt上的9.2)支持-mno-gas-isr-prologues,以使GCC显示与上述Atmel Studio中的反汇编相匹配的真实的指令。因此,如果有人想玩这个,在https://godbolt.org/z/q6M518qfP中有效果的东西在真实的Atmel Studio中可能会有同样的效果。

xghobddn

xghobddn1#

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

平均-gcc ABI:R 0和R1建模、使用和优势

当avr-gcc ABI被设计出来的时候,决定将R 0和R1建模为固定寄存器。*“固定寄存器 *”意味着编译器不会在寄存器分配或其他任何方式中使用它们。这些寄存器的唯一使用是在编译的最后阶段,当汇编代码被打印到*.s时,其中这些寄存器可以在各自的输出字符串中隐式使用。这基本上与通过内联汇编输出的指令相同,对编译器是不透明的。
选择此选项的原因是,通过使用这些额外的寄存器可以提高代码的整体质量,其中R 0用作临时寄存器(即__tmp_reg__),而R1(即__zero_reg__)包含值0。例如,要比较寄存器%0中的16位整数与42,您只需

cpi %A0, 42
cpc %B0, __zero_reg__

而不需要任何进一步的麻烦,即不需要分配某个临时寄存器、清除它等。

R 0和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序言和结语,因为无法分析R 0、R1是否被使用或更改,SREG也是如此。因此,经典的ISR序言必须

  • 保存R 0、R1和SREG。
  • 将R1设置为0(因为它可能临时保存非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-GCCV 8 + X1 M7 N1 X的汇编代码:

__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已关闭等,则不生成__gcc_isr
  • 如果ISR具有开放代码调用或执行非本地后藤(setjmp / longjmp)之类的奇怪操作,则不要生成__gcc_isr
  • 如果一切顺利,则打印__gcc_isr伪指令,而不是实际的ISR序言/结语。
    汇编程序的作用:
  • 它分析从序言块__gcc_isr 1开始直到最后块0的八个完整ISR代码,并记录R 0、R1的使用情况以及对SREG的影响。
  • 不分析函数调用背后的代码:如果遇到[r]call,假设R 0、R1和SREG的情况最糟。编译器已经处理了尾部调用(通过某个跳转指令进行的调用)。
  • 根据R 0、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来做这项工作的原因,但这只是一个不错的副作用。DR为什么我们用气体作为工作马。
内联asm必须将所有副作用都显式化,这是正确的,但要有所保留:
在cc 0 →CCmode transition之前,没有条件码寄存器可以重写,所以假设基本上每个insn都会重写cc 0,这种情况在引入CCmode后没有太大变化(实际上变得更糟了):比较insn正在设置CC,但是除了分支或超简单1指令insn之外,几乎每隔一个insn都在重击CC。
原因是很多insn都有非常复杂的insn输出打印机,例如用于特定的算术运算或多字节加载/存储。不可能通过任何合理的工作量来在该级别上建模精确的CC行为,因此只假设CC乱码。这也适用于内联asm:自从CCmode出现以来,avr后端只是将“cc”乱码添加到 * 所有内联汇编 * 中,这样遗留代码就不会中断,请参见avr.cc。

tmp_reg的情况类似:Insn打印机将在当时和何时隐式地使用它,因此编译器无法以任何合理的精度计算出它的使用/乱码状态,即使 * 它 * 是一个普通的、已分配的寄存器,而不是一个固定的寄存器。
zero_reg也是一样的,它也是固定的。一些insn打印机只在特殊情况下使用它,而且也不可能以合理的方式对此建模。正如您已经注意到的,insns(和inline asm)可能会假设zero_reg = 0,这就是为什么ISR使用单个

asm ("sts 0,__zero_reg__");

将工作并神奇地示例化zero_reg。
当然,不可能在内联asm中添加像"r" (0)这样的隐式操作数--即使可能,这也会破坏现有代码。而且,对R 0或R1进行重敲仍然是无效的,因为它们是固定的,所以我们不希望依赖于重敲器的存在。从技术上讲,对zero_reg进行重敲,然后将其恢复为0的内联asm不会对它进行重敲。然而,ISR仍然需要知道。

相关问题