C中的空循环。编译器是否生成了大量不必要的代码,或者我错过了什么?

时间:2017-07-01 07:57:42

标签: c assembly compilation avr

我是AVR汇编语言的新手,并决定查看用C编写的哑函数延迟函数的代码,看看有多长算术的空循环可以花多长时间。

延迟功能如下:

"permissions" : [
    "*://example.domain.com/v1/*"
  ],

我用void delay(uint32_t cycles) { for (volatile uint32_t i = 0; i < cycles; i++) {} } 反汇编它,我认为,得到了一些奇怪的结果(见评论中的四个问题):

objdump

毕竟,为什么它需要所有这些行动?

UPD

完整代码:

00000080 <delay>:
void delay (uint32_t cycles) {                  
; `cycles` is stored in r22..r25
  80:   cf 93           push    r28
  82:   df 93           push    r29
; First one: why does the compiler rcall the next position relative to the following
; two instructions? Some stack management?
  84:   00 d0           rcall   .+0             ; 0x86 <delay+0x6>
  86:   00 d0           rcall   .+0             ; 0x88 <delay+0x8>
  88:   cd b7           in      r28, 0x3d       ; 61
  8a:   de b7           in      r29, 0x3e       ; 62
  8c:   ab 01           movw    r20, r22
  8e:   bc 01           movw    r22, r24
; Now `cycles` is in r20..r23
    for (volatile uint32_t i = 0; i < cycles; i++) {}
; r1 was earlier initialized with zero by `eor r1, r1`
; `i` is in r24..r27
  90:   19 82           std     Y+1, r1 ; 0x01
  92:   1a 82           std     Y+2, r1 ; 0x02
  94:   1b 82           std     Y+3, r1 ; 0x03
  96:   1c 82           std     Y+4, r1 ; 0x04
  98:   89 81           ldd     r24, Y+1        ; 0x01
  9a:   9a 81           ldd     r25, Y+2        ; 0x02
  9c:   ab 81           ldd     r26, Y+3        ; 0x03
  9e:   bc 81           ldd     r27, Y+4        ; 0x04
  a0:   84 17           cp      r24, r20
  a2:   95 07           cpc     r25, r21
  a4:   a6 07           cpc     r26, r22
  a6:   b7 07           cpc     r27, r23
  a8:   a0 f4           brcc    .+40            ; 0xd2 <delay+0x52>, to location A
; location B:
; Third (yes, before the second) one: why does it load the registers each time after
; comparing the counter with the limit if `cp`, `cpc` do not change the registers?
  aa:   89 81           ldd     r24, Y+1        ; 0x01
  ac:   9a 81           ldd     r25, Y+2        ; 0x02
  ae:   ab 81           ldd     r26, Y+3        ; 0x03
  b0:   bc 81           ldd     r27, Y+4        ; 0x04
  b2:   01 96           adiw    r24, 0x01       ; 1
  b4:   a1 1d           adc     r26, r1
  b6:   b1 1d           adc     r27, r1
; Second one: why does it store and load the same registers with unchanged values?
; If it needs to store the registers, why does it load anyway? Does `std` change the
; source registers?
  b8:   89 83           std     Y+1, r24        ; 0x01
  ba:   9a 83           std     Y+2, r25        ; 0x02
  bc:   ab 83           std     Y+3, r26        ; 0x03
  be:   bc 83           std     Y+4, r27        ; 0x04
  c0:   89 81           ldd     r24, Y+1        ; 0x01
  c2:   9a 81           ldd     r25, Y+2        ; 0x02
  c4:   ab 81           ldd     r26, Y+3        ; 0x03
  c6:   bc 81           ldd     r27, Y+4        ; 0x04
  c8:   84 17           cp      r24, r20
  ca:   95 07           cpc     r25, r21
  cc:   a6 07           cpc     r26, r22
  ce:   b7 07           cpc     r27, r23
  d0:   60 f3           brcs    .-40            ; 0xaa <delay+0x2a>, to location B
}
; Location A:
; Finally, fourth one: so, under my first question it issued an `rcall` twice and now 
; just pops the return addresses to nowhere? Now the `rcall`s are double-strange
  d2:   0f 90           pop     r0
  d4:   0f 90           pop     r0
  d6:   0f 90           pop     r0
  d8:   0f 90           pop     r0
  da:   df 91           pop     r29
  dc:   cf 91           pop     r28
  de:   08 95           ret

编译器:#include <avr/io.h> void delay (uint32_t cycles) { for (volatile uint32_t i = 0; i < cycles; i++) {} } int main(void) { DDRD |= 1 << DDD2 | 1 << DDD3 | 1 << DDD4 | 1 << DDD5; PORTD |= 1 << PORTD2 | 1 << PORTD4; while (1) { const uint32_t d = 1000000; delay(d); PORTD ^= 1 << PORTD2 | 1 << PORTD3; delay(d); PORTD ^= 1 << PORTD4 | 1 << PORTD5; delay(d); PORTD ^= 1 << PORTD3 | 1 << PORTD2; delay(d); PORTD ^= 1 << PORTD5 | 1 << PORTD4; } }

构建命令:

gcc version 5.4.0 (AVR_8_bit_GNU_Toolchain_3.6.0_1734)

对有关延迟功能的注意事项的回复:

是的,我完全理解这种延迟功能方法可能存在的问题,即不可预测的时序和优化周期的风险。这只是一个自学的例子,可以看到一个空循环被编译成什么

2 个答案:

答案 0 :(得分:4)

首先,请注意使用像这样的繁忙循环写入延迟并不好,因为时间将取决于编译器如何操作的细节。对于AVR平台,请使用avr-libc和GCC提供的内置延迟功能,如JLH的回答所述。

双重呼叫和四个弹出

通常,函数顶部的rcall +0指令可以方便地将函数运行的次数加倍。但是在这种情况下,我们可以看到返回地址没有被返回,实际上它们在函数末尾被删除了四个pop指令。

因此,在函数的开头,编译器将四个字节添加到堆栈中,并在函数的末尾从堆栈中删除四个字节。这是编译器为变量i分配存储的方式。由于i是局部变量,因此它通常存储在堆栈中。编译器优化可能允许将变量存储在寄存器中,但我不认为volatile变量允许这样的优化。这回答了你的第一和第四个问题。

额外装载和存储

您将变量i标记为volatile,它告诉编译器它不能对i存储的内存做出任何假设。每次代码读取或写入{ {1}},编译器必须对保存i的RAM位置生成实际读取或写入;它不被允许进行你认为会做出的优化。这回答了你的第二个和第三个问题。

i关键字对芯片上的特殊功能寄存器很有用,对主循环和中断之间共享的变量很有用。

答案 1 :(得分:3)

不确定您正在使用哪种编译器,但Atmel Studio下的GCC为其原生延迟功能提供了以下功能。首先,我的C代码:

#define F_CPU 20000000
#include <util/delay.h>

int main(void)
{
    while (1) 
    {
        _delay_us(100);
    }
}

由此产生的反汇编代码部分:

    __builtin_avr_delay_cycles(__ticks_dc);
  f6:   83 ef           ldi r24, 0xF3   ; 243
  f8:   91 e0           ldi r25, 0x01   ; 1
  fa:   01 97           sbiw    r24, 0x01   ; 1
  fc:   f1 f7           brne    .-4         ; 0xfa <main+0x4>
  fe:   00 c0           rjmp    .+0         ; 0x100 <main+0xa>
 100:   00 00           nop
 102:   f9 cf           rjmp    .-14        ; 0xf6 <main>

这里我只延迟了100微秒,但是如果我把它改为100毫秒,我仍然不会像你的那样冗长:

    __builtin_avr_delay_cycles(__ticks_dc);
  f6:   2f e7           ldi r18, 0x7F   ; 127
  f8:   8a e1           ldi r24, 0x1A   ; 26
  fa:   96 e0           ldi r25, 0x06   ; 6
  fc:   21 50           subi    r18, 0x01   ; 1
  fe:   80 40           sbci    r24, 0x00   ; 0
 100:   90 40           sbci    r25, 0x00   ; 0
 102:   e1 f7           brne    .-8         ; 0xfc <main+0x6>
 104:   00 c0           rjmp    .+0         ; 0x106 <main+0x10>
 106:   00 00           nop
 108:   f6 cf           rjmp    .-20        ; 0xf6 <main>

结论:不确定为什么你这么长,但是如果你想要更严格的代码,并且你的编译器有一个内置的实现,那么使用编译器的实现作为你如何做这些延迟的模型。