AVR的 GCC编译器在不需要时会创建中间计数变量
我在Debian上使用avr-gcc 14.2时遇到了一个优化问题。
我有以下代码(为了便于概览,我对代码和立即汇编进行了缩短):
void function(uint8_t byte){
cli();
uint8_t bit = 0x01;
while (1) {
// for (uint8_t bit = 0x01; bit != 0; bit <<=1) {
if (byte & bit) { // Write high
// do something not using byte or bit variables
} else { // Write low
// do something else not using byte or bit variables
}
bit = bit << 1;
if (bit == 0)
break;
}
sei();
}
avr-gcc生成的立即汇编来自output.elf.ltrans0.ltrans.s:
cli
; 0 "" 2
/* #NOAPP */
ldi r18,lo8(8) ; ivtmp_65,
ldi r19,0 ; ivtmp_65
; routine.c:93: uint8_t bit = 0x01;
ldi r25,lo8(1) ; bit,
.L42:
;removed code prior to if related to what happens inside if and else blocks
mov r20,r25 ; _9, bit
and r20,r22 ; _9, byte
; routine.c:97: if (byte & bit) {
cp r20, __zero_reg__ ; _9
breq .L40 ; ,
;removed code if condition is true
.L41:
; routine.c:118: bit = bit << 1;
lsl r25 ; bit
; routine.c:119: if (bit == 0)
subi r18, 1 ; ivtmp_65,
sbci r19, 0 ; ivtmp_65,
brne .L42 ; ,
; routine.c:123: sei();
/* #APP */
; 123 "routine.c" 1
sei
.L40:
; removed code if condition is not true
rjmp .L41
使用 while 或 for 的实现都会得到相同的代码。
avr-gcc强制引入一个16位的临时计数变量(r18、r19),从8 递减到0,而不是按C 代码中的逻辑在变量bit(r25) 等于0 时就中断循环;如果编译器使用左移操作 lsl,那么变量bit(r25) 在8 次移位后就会变为0。
现在我在想该如何去掉这个临时计数变量?这是编译器的bug吗?编译设置错了?应该查看的文件错了?我是不是遗漏了什么?
编译器标志为:
-c -mmcu=avr32dd14 -DF_CPU=8000000UL -Os -Wall -Wstrict-prototypes -Wundef -Wunreachable-code -Wsign-compare -Wextra -std=gnu99 -fverbose-asm -save-temps -flto -Wundef
链接器标志为:
-mmcu=avr32dd14 -DF_CPU=8000000UL -Os -Wall -Wstrict-prototypes -Wundef -Wunreachable-code -Wsign-compare -Wextra -std=gnu99 -fverbose-asm -save-temps -flto -Wundef -Wl,-Map=output.map,--cref -lm -Wl,--gc-sections,--relax -flto -fwhole-program
解决方案
我刚刚在添加了一些缺失头文件后,用avr-gcc v14.3编译了你的代码:
#include <stdint.h> // Added because otherwise the code doesn't compile.
#include <avr/interrupt.h> // dito
void function(uint8_t byte){
cli();
uint8_t bit = 0x01;
while (1) {
// for (uint8_t bit = 0x01; bit != 0; bit <<=1) {
if (byte & bit) { // Write high
// do something not using byte or bit variables
} else { // Write low
// do something else not using byte or bit variables
}
bit = bit << 1;
if (bit == 0)
break;
}
sei();
}
生成的汇编如下:
function:
/* #APP */
cli
sei
/* #NOAPP */
/* epilogue start */
ret
因此一切都按预期。
也许你的Makefile里有bug,你看到的代码是来自过时的源文件所生成的。
除此之外,除了 cli 和 sei 外,你的代码没有任何副作用。也许你在以前的缓冲区里还有别的代码,而不是在文件里。
一个可工作的测试用例
下面是一个测试用例,可能与你在某处看到的情况相符:
#include <stdint.h>
char x;
void func (uint8_t byte)
{
for (uint8_t bit = 0x01; bit != 0; bit <<= 1)
if (byte & bit)
x = 0;
else
x = 1;
}
它省略了 cli 和 sei,因为它们对优化没有影响。使用 -Os -mmcu=avr32dd14 时,avr-gcc v14生成了这段汇编:
ldi r18,lo8(8) ; ivtmp_4,
ldi r19,0 ; ivtmp_4
; x.c:29: for (uint8_t bit = 0x01; bit != 0; bit <<= 1)
ldi r25,lo8(1) ; bit,
.L3:
; x.c:30: if (byte & bit)
ldi r21,lo8(1) ; _3,
mov r20,r24 ; _1, byte
and r20,r25 ; _1, bit
cpse r20,__zero_reg__ ; _1
ldi r21,0 ; _3
.L2:
; x.c:29: for (uint8_t bit = 0x01; bit != 0; bit <<= 1)
lsl r25 ; bit
; x.c:29: for (uint8_t bit = 0x01; bit != 0; bit <<= 1)
subi r18, 1 ; ivtmp_4,
sbci r19, 0 ; ivtmp_4,
brne .L3 ; ,
sts x,r21 ; x, _3
正如所见,编译器引入了一个从8 到0 的16位自举变量 ivtmp_4。在很多情况下,引入这样的自举变量会使代码的效率低于朴素的循环,参见 PR116548;例如,在使用 -fno-tree-loop-optimize 时:
; x.c:29: for (uint8_t bit = 0x01; bit != 0; bit <<= 1)
ldi r25,lo8(1) ; bit,
.L3:
; x.c:30: if (byte & bit)
ldi r19,lo8(1) ; _15,
mov r18,r24 ; _1, byte
and r18,r25 ; _1, bit
cpse r18,__zero_reg__ ; _1
ldi r19,0 ; _15
.L2:
; x.c:29: for (uint8_t bit = 0x01; bit != 0; bit <<= 1)
lsl r25 ; bit
; x.c:29: for (uint8_t bit = 0x01; bit != 0; bit <<= 1)
cpse r25,__zero_reg__ ; bit,
rjmp .L3 ;
sts x,r19 ; x, _15
(cpse 是从cc0→CCmode转换中产生的副作用;avr-gcc v8和 v15+使用 brne,它们的代码少1 条指令。)
另外,顺便提一下,这个测试用例在avr-gcc v16上不工作,因为它只会生成
; x.c:30: if (byte & bit)
cpi r24,0x80 ; byte
sbc r24,r24 ; _10
neg r24 ; _10
sts x,r24 ; x, _10