AVR的 GCC编译器在不需要时会创建中间计数变量

编程语言 2026-07-09

我在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

使用 whilefor 的实现都会得到相同的代码。

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,你看到的代码是来自过时的源文件所生成的。

除此之外,除了 clisei 外,你的代码没有任何副作用。也许你在以前的缓冲区里还有别的代码,而不是在文件里。


一个可工作的测试用例

下面是一个测试用例,可能与你在某处看到的情况相符:

#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;
}

它省略了 clisei,因为它们对优化没有影响。使用 -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
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章