C 中的一个空循环。编译器是否生成了很多不必要的代码,或者我错过了什么?
An empty cycle in C. Does the compiler generate lots of unnecessary code or did I miss something?
我是 AVR 汇编语言的新手,因此决定查看用 C 编写的一个愚蠢的延迟函数的代码,看看一个带有长算术的空循环需要多长时间。
延时函数如下:
void delay(uint32_t cycles) {
for (volatile uint32_t i = 0; i < cycles; i++) {}
}
我用objdump
反汇编了它,我认为得到了一些奇怪的结果(见评论中的四个问题):
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
毕竟,为什么它需要所有这些操作?
UPD
完整代码:
#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)
构建命令:
avr-gcc.exe -x c -funsigned-char -funsigned-bitfields -DDEBUG -I%inc_folder% -O1 -ffunction-sections -fdata-sections -fpack-struct -fshort-enums -g2 -Wall -mmcu=atmega328p -B %atmega328p_folder% -c -std=gnu99 -MD -MP %sources, object files, etc%
回复关于延时功能的注意事项:
是的,我完全理解延迟函数的这种方法可能存在的问题,即时间不可预测和优化周期的风险。这只是一个自学例子,看看空循环编译成什么
不确定您使用的是什么编译器,但 Atmel Studio 下的 GCC 为其本机 delay 函数提供了以下内容。首先是我的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>
结论:不确定为什么你的代码这么长,但如果你想要更紧凑的代码,并且你的编译器有一个内置的实现,请使用编译器的实现作为你如何执行这些延迟的模型。
首先,请注意使用这样一个繁忙的循环来编写延迟是不好的,因为时间将取决于您的编译器如何运行的细节。对于AVR平台,使用avr-libc和GCC提供的内置延时函数,详见JLH的回答。
两次 rcall 和四次 pops
通常,函数顶部的 rcall +0
指令是使函数运行次数加倍的简便方法。但是在这种情况下,我们可以看到 return 地址没有被 return 编辑,它们实际上是在函数末尾用四个 pop
指令从堆栈中删除的.
所以在函数的开头,编译器将四个字节添加到堆栈,在函数的结尾从堆栈中删除四个字节。这就是编译器为您的变量 i
分配存储空间的方式。由于 i
是一个局部变量,它通常存储在堆栈中。编译器优化可能允许将变量存储在寄存器中,但我认为 volatile
变量不允许这样的优化。这回答了你的第一个和第四个问题。
额外加载和存储
您将变量 i
标记为 volatile
,这告诉编译器它不能对存储 i
的内存做出任何假设。每次您的代码读取或写入i
,编译器必须对保存 i
的 RAM 位置生成真正的读取或写入;不允许进行您认为会进行的优化。这回答了你的第二个和第三个问题。
volatile
关键字对于芯片上的特殊功能寄存器很有用,对于在主循环和中断之间共享的变量也很有用。
我是 AVR 汇编语言的新手,因此决定查看用 C 编写的一个愚蠢的延迟函数的代码,看看一个带有长算术的空循环需要多长时间。
延时函数如下:
void delay(uint32_t cycles) {
for (volatile uint32_t i = 0; i < cycles; i++) {}
}
我用objdump
反汇编了它,我认为得到了一些奇怪的结果(见评论中的四个问题):
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
毕竟,为什么它需要所有这些操作?
UPD
完整代码:
#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)
构建命令:
avr-gcc.exe -x c -funsigned-char -funsigned-bitfields -DDEBUG -I%inc_folder% -O1 -ffunction-sections -fdata-sections -fpack-struct -fshort-enums -g2 -Wall -mmcu=atmega328p -B %atmega328p_folder% -c -std=gnu99 -MD -MP %sources, object files, etc%
回复关于延时功能的注意事项:
是的,我完全理解延迟函数的这种方法可能存在的问题,即时间不可预测和优化周期的风险。这只是一个自学例子,看看空循环编译成什么
不确定您使用的是什么编译器,但 Atmel Studio 下的 GCC 为其本机 delay 函数提供了以下内容。首先是我的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>
结论:不确定为什么你的代码这么长,但如果你想要更紧凑的代码,并且你的编译器有一个内置的实现,请使用编译器的实现作为你如何执行这些延迟的模型。
首先,请注意使用这样一个繁忙的循环来编写延迟是不好的,因为时间将取决于您的编译器如何运行的细节。对于AVR平台,使用avr-libc和GCC提供的内置延时函数,详见JLH的回答。
两次 rcall 和四次 pops
通常,函数顶部的 rcall +0
指令是使函数运行次数加倍的简便方法。但是在这种情况下,我们可以看到 return 地址没有被 return 编辑,它们实际上是在函数末尾用四个 pop
指令从堆栈中删除的.
所以在函数的开头,编译器将四个字节添加到堆栈,在函数的结尾从堆栈中删除四个字节。这就是编译器为您的变量 i
分配存储空间的方式。由于 i
是一个局部变量,它通常存储在堆栈中。编译器优化可能允许将变量存储在寄存器中,但我认为 volatile
变量不允许这样的优化。这回答了你的第一个和第四个问题。
额外加载和存储
您将变量 i
标记为 volatile
,这告诉编译器它不能对存储 i
的内存做出任何假设。每次您的代码读取或写入i
,编译器必须对保存 i
的 RAM 位置生成真正的读取或写入;不允许进行您认为会进行的优化。这回答了你的第二个和第三个问题。
volatile
关键字对于芯片上的特殊功能寄存器很有用,对于在主循环和中断之间共享的变量也很有用。