[英]Does arm-none-eabi-gcc produce slower code than Keil uVision
我有一个简单的闪烁 LED 程序在 STM32f103C8 上运行(没有初始化样板):
void soft_delay(void) {
for (volatile uint32_t i=0; i<2000000; ++i) { }
}
uint32_t iters = 0;
while (1)
{
LL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
soft_delay();
++iters;
}
它是用Keil uVision v.5
(默认编译器)和CLion
使用arm-none-eabi-gcc
编译器编译的。 令人惊讶的是 arm-none-eabi-gcc 程序在发布模式 (-O2 -flto) 下运行速度慢 50%,在调试模式下运行速度慢 100%。
我怀疑有3个原因:
Keil过度优化(不太可能,因为代码很简单)
由于错误的编译器标志,arm-none-eabi-gcc 优化不足(我使用 CLion Embedded plugins` CMakeLists.txt)
初始化中的一个错误,使芯片具有较低的时钟频率与 arm-none-eabi-gcc(有待调查)
我还没有潜入优化和反汇编的丛林中,我希望有很多有经验的嵌入式开发人员已经遇到过这个问题并有答案。
更新 1
使用 Keil ArmCC 的不同优化级别,我看到了它如何影响生成的代码。 它会极大地影响,尤其是执行时间。 以下是针对每个优化级别的soft_delay()
函数的基准测试和反汇编(RAM 和 Flash 数量包括初始化代码)。
-O0:RAM:1032,闪存:1444,执行时间(20 次迭代):18.7 秒
soft_delay PROC
PUSH {r3,lr}
MOVS r0,#0
STR r0,[sp,#0]
B |L6.14|
|L6.8|
LDR r0,[sp,#0]
ADDS r0,r0,#1
STR r0,[sp,#0]
|L6.14|
LDR r1,|L6.24|
LDR r0,[sp,#0]
CMP r0,r1
BCC |L6.8|
POP {r3,pc}
ENDP
-O1:RAM:1032,闪存:1216,执行时间(20 次迭代):13.3 秒
soft_delay PROC
PUSH {r3,lr}
MOVS r0,#0
STR r0,[sp,#0]
LDR r0,|L6.24|
B |L6.16|
|L6.10|
LDR r1,[sp,#0]
ADDS r1,r1,#1
STR r1,[sp,#0]
|L6.16|
LDR r1,[sp,#0]
CMP r1,r0
BCC |L6.10|
POP {r3,pc}
ENDP
-O2 -Otime:RAM:1032,闪存:1136,执行时间(20 次迭代):9.8 秒
soft_delay PROC
SUB sp,sp,#4
MOVS r0,#0
STR r0,[sp,#0]
LDR r0,|L4.24|
|L4.8|
LDR r1,[sp,#0]
ADDS r1,r1,#1
STR r1,[sp,#0]
CMP r1,r0
BCC |L4.8|
ADD sp,sp,#4
BX lr
ENDP
-O3:RAM:1032,闪存:1176,执行时间(20 次迭代):9.9 秒
soft_delay PROC
PUSH {r3,lr}
MOVS r0,#0
STR r0,[sp,#0]
LDR r0,|L5.20|
|L5.8|
LDR r1,[sp,#0]
ADDS r1,r1,#1
STR r1,[sp,#0]
CMP r1,r0
BCC |L5.8|
POP {r3,pc}
ENDP
TODO: arm-none-eabi-gcc
基准测试和反汇编。
第二个答案是对可能影响 OP 可能看到的性能结果的各种事情的演示,以及可能测试那些 STM32F103C8 蓝色药丸的示例。
完整的源代码:
闪存文件
MEMORY
{
rom : ORIGIN = 0x08000000, LENGTH = 0x1000
ram : ORIGIN = 0x20000000, LENGTH = 0x1000
}
SECTIONS
{
.text : { *(.text*) } > rom
.rodata : { *(.rodata*) } > rom
.bss : { *(.bss*) } > ram
}
flash.s
.cpu cortex-m0
.thumb
.thumb_func
.global _start
_start:
stacktop: .word 0x20001000
.word reset
.word hang
.word hang
.thumb_func
reset:
bl notmain
b hang
.thumb_func
hang: b .
.align
.thumb_func
.globl PUT32
PUT32:
str r1,[r0]
bx lr
.thumb_func
.globl GET32
GET32:
ldr r0,[r0]
bx lr
.thumb_func
.globl dummy
dummy:
bx lr
测试.s
.cpu cortex-m0
.thumb
.word 0,0,0
.word 0,0,0,0
.thumb_func
.globl TEST
TEST:
bx lr
不是main.c
//PA9 TX
//PA10 RX
void PUT32 ( unsigned int, unsigned int );
unsigned int GET32 ( unsigned int );
void dummy ( unsigned int );
#define USART1_BASE 0x40013800
#define USART1_SR (USART1_BASE+0x00)
#define USART1_DR (USART1_BASE+0x04)
#define USART1_BRR (USART1_BASE+0x08)
#define USART1_CR1 (USART1_BASE+0x0C)
#define USART1_CR2 (USART1_BASE+0x10)
#define USART1_CR3 (USART1_BASE+0x14)
//#define USART1_GTPR (USART1_BASE+0x18)
#define GPIOA_BASE 0x40010800
#define GPIOA_CRH (GPIOA_BASE+0x04)
#define RCC_BASE 0x40021000
#define RCC_APB2ENR (RCC_BASE+0x18)
#define STK_CSR 0xE000E010
#define STK_RVR 0xE000E014
#define STK_CVR 0xE000E018
#define STK_MASK 0x00FFFFFF
static void uart_init ( void )
{
//assuming 8MHz clock, 115200 8N1
unsigned int ra;
ra=GET32(RCC_APB2ENR);
ra|=1<<2; //GPIOA
ra|=1<<14; //USART1
PUT32(RCC_APB2ENR,ra);
//pa9 TX alternate function output push-pull
//pa10 RX configure as input floating
ra=GET32(GPIOA_CRH);
ra&=~(0xFF0);
ra|=0x490;
PUT32(GPIOA_CRH,ra);
PUT32(USART1_CR1,0x2000);
PUT32(USART1_CR2,0x0000);
PUT32(USART1_CR3,0x0000);
//8000000/16 = 500000
//500000/115200 = 4.34
//4 and 5/16 = 4.3125
//4.3125 * 16 * 115200 = 7948800
PUT32(USART1_BRR,0x0045);
PUT32(USART1_CR1,0x200C);
}
static void uart_putc ( unsigned int c )
{
while(1)
{
if(GET32(USART1_SR)&0x80) break;
}
PUT32(USART1_DR,c);
}
static void hexstrings ( unsigned int d )
{
//unsigned int ra;
unsigned int rb;
unsigned int rc;
rb=32;
while(1)
{
rb-=4;
rc=(d>>rb)&0xF;
if(rc>9) rc+=0x37; else rc+=0x30;
uart_putc(rc);
if(rb==0) break;
}
uart_putc(0x20);
}
static void hexstring ( unsigned int d )
{
hexstrings(d);
uart_putc(0x0D);
uart_putc(0x0A);
}
void soft_delay(void) {
for (volatile unsigned int i=0; i<2000000; ++i) { }
}
int notmain ( void )
{
PUT32(STK_CSR,4);
PUT32(STK_RVR,0x00FFFFFF);
PUT32(STK_CVR,0x00000000);
PUT32(STK_CSR,5);
uart_init();
hexstring(0x12345678);
hexstring(GET32(0xE000E018));
hexstring(GET32(0xE000E018));
return(0);
}
建造
arm-none-eabi-as --warn --fatal-warnings -mcpu=cortex-m3 flash.s -o flash.o
arm-none-eabi-as --warn --fatal-warnings -mcpu=cortex-m3 test.s -o test.o
arm-none-eabi-gcc -Wall -Werror -O2 -nostdlib -nostartfiles -ffreestanding -mthumb -mcpu=cortex-m0 -march=armv6-m -c notmain.c -o notmain.thumb.o
arm-none-eabi-ld -o notmain.thumb.elf -T flash.ld flash.o test.o notmain.thumb.o
arm-none-eabi-objdump -D notmain.thumb.elf > notmain.thumb.list
arm-none-eabi-objcopy notmain.thumb.elf notmain.thumb.bin -O binary
arm-none-eabi-gcc -Wall -Werror -O2 -nostdlib -nostartfiles -ffreestanding -mthumb -mcpu=cortex-m3 -march=armv7-m -c notmain.c -o notmain.thumb2.o
arm-none-eabi-ld -o notmain.thumb2.elf -T flash.ld flash.o test.o notmain.thumb2.o
arm-none-eabi-objdump -D notmain.thumb2.elf > notmain.thumb2.list
arm-none-eabi-objcopy notmain.thumb2.elf notmain.thumb2.bin -O binary
UART输出如图
12345678
00FFE445
00FFC698
如果我接受您的代码,请缩短它,不要整天。
void soft_delay(void) {
for (volatile unsigned int i=0; i<0x2000; ++i) { }
}
arm-none-eabi-gcc -c -O0 -mthumb -mcpu=cortex-m0 hello.c -o hello.o
是的,我知道这是一个 m3
arm-none-eabi-gcc --version
arm-none-eabi-gcc (GCC) 5.4.0
给
00000000 <soft_delay>:
0: b580 push {r7, lr}
2: b082 sub sp, #8
4: af00 add r7, sp, #0
6: 2300 movs r3, #0
8: 607b str r3, [r7, #4]
a: e002 b.n 12 <soft_delay+0x12>
c: 687b ldr r3, [r7, #4]
e: 3301 adds r3, #1
10: 607b str r3, [r7, #4]
12: 687b ldr r3, [r7, #4]
14: 4a03 ldr r2, [pc, #12] ; (24 <soft_delay+0x24>)
16: 4293 cmp r3, r2
18: d9f8 bls.n c <soft_delay+0xc>
1a: 46c0 nop ; (mov r8, r8)
1c: 46bd mov sp, r7
1e: b002 add sp, #8
20: bd80 pop {r7, pc}
22: 46c0 nop ; (mov r8, r8)
24: 00001fff
首先检查测试基础设施
.cpu cortex-m0
.thumb
.align 8
.word 0,0
.thumb_func
.globl TEST
TEST:
push {r4,r5,r6,lr}
mov r4,r0
mov r5,r1
ldr r6,[r4]
inner:
bl soft_delay
sub r5,#1
bne inner
ldr r3,[r4]
sub r0,r6,r3
pop {r4,r5,r6,pc}
.align 8
soft_delay:
bx lr
在 openocd telnet 窗口中
reset halt
flash write_image erase notmain.thumb.elf
reset
给
12345678
00001B59
7001 个时钟,假设 systick 与 cpu 匹配,即 7001 个手臂时钟,每个循环 4 条指令。
退一步注意我对齐了一些东西
08000108 <TEST>:
8000108: b570 push {r4, r5, r6, lr}
800010a: 1c04 adds r4, r0, #0
800010c: 1c0d adds r5, r1, #0
800010e: 6826 ldr r6, [r4, #0]
08000110 <inner>:
8000110: f000 f876 bl 8000200 <soft_delay>
8000114: 3d01 subs r5, #1
8000116: d1fb bne.n 8000110 <inner>
8000118: 6823 ldr r3, [r4, #0]
800011a: 1af0 subs r0, r6, r3
800011c: bd70 pop {r4, r5, r6, pc}
08000200 <soft_delay>:
8000200: 4770 bx lr
两个循环都很好地对齐。
现在,如果我这样做:
0800010a <TEST>:
800010a: b570 push {r4, r5, r6, lr}
800010c: 1c04 adds r4, r0, #0
800010e: 1c0d adds r5, r1, #0
8000110: 6826 ldr r6, [r4, #0]
08000112 <inner>:
8000112: f000 f875 bl 8000200 <soft_delay>
8000116: 3d01 subs r5, #1
8000118: d1fb bne.n 8000112 <inner>
800011a: 6823 ldr r3, [r4, #0]
800011c: 1af0 subs r0, r6, r3
800011e: bd70 pop {r4, r5, r6, pc}
只需更改应该测试被测代码的代码的对齐方式,我现在得到:
00001F40
8000 滴答来执行该循环 1000 次,该调用与被测代码函数仍然对齐
08000200 <soft_delay>:
8000200: 4770 bx lr
.align 8,一般不要在gnu 上使用带有数字的.align 其行为不会跨目标转换。 .balign 更好。 反正我用过。 这两个词是因为 align 使 TEST 对齐,但 inner 是我想要对齐的,所以我添加了两个词使其对齐。
.align 8
.word 0,0
nop
.thumb_func
.globl TEST
TEST:
push {r4,r5,r6,lr}
mov r4,r0
mov r5,r1
ldr r6,[r4]
inner:
bl soft_delay
sub r5,#1
bne inner
ldr r3,[r4]
sub r0,r6,r3
pop {r4,r5,r6,pc}
一点代码审查,以确保我没有在这里犯错。
r0 是系统棒当前值寄存器
r1 是我想运行被测代码的循环数
调用约定允许 r0-r3 被破坏,因此我需要将 r0 和 r1 移动到非易失性寄存器(根据调用约定)。
我想对循环之前的指令和之后的指令进行采样。
所以我需要两个用于 r0 和 r1 的寄存器和一个用于存储开始时间的寄存器,因此 r4、r5、r6 非常适合将偶数个寄存器压入堆栈。 必须保存 lr 以便我们可以返回。
我们现在可以安全地在循环中调用 soft_delay,减去计数,如果不等于内部,则分支,一旦计数完成,读取 r3 中的计时器。 从上面的输出这是一个递减计数器,所以从头开始减去结束,从技术上讲,因为这是一个 24 位计数器,我应该使用 0x00FFFFFF 来正确地进行减法运算,但是因为这不会翻转,所以我可以假设该操作。 结果/返回值进入 r0,弹出所有内容,包括弹出 pc 以返回到打印出 r0 值的 C 调用函数。
我认为测试代码很好。
读取 CPUID 寄存器
411FC231
所以这意味着 r1p1,虽然我使用的 TRM 是为 r2p1 编写的,但您必须非常小心地使用正确的文档,但有时也会使用当前文档或两者之间的所有文档(如果有的话)以查看发生了什么变化。
ICode 存储器接口
从代码存储空间 0x00000000 到 0x1FFFFFFF 的指令提取通过 32 位 AHB-Lite 总线执行。 调试器无法访问此接口。 所有提取都是字宽的。 每个字提取的指令数取决于运行的代码和内存中代码的对齐方式。
有时在 ARM TRM 中,您会在处理器功能附近的顶部看到获取信息,这告诉我我想知道什么。
08000112 <inner>:
8000112: f000 f875 bl 8000200 <soft_delay>
8000116: 3d01 subs r5, #1
8000118: d1fb bne.n 8000112 <inner>
这需要在 110、114 和 118 处获取。
08000110 <inner>:
8000110: f000 f876 bl 8000200 <soft_delay>
8000114: 3d01 subs r5, #1
8000116: d1fb bne.n 8000110 <inner>
这是在 110 和 114 处的一次提取,但不是 118 处的一次提取,因此额外的提取可能是我们添加的时钟。 m3 是第一个公开可用的,它的许多核心功能已经消失,但类似的功能又回来了。 一些较小的内核以不同的方式获取,您看不到这种对齐问题。 使用更大的内核,例如全尺寸的内核,它们有时会一次获取 4 或 8 条指令,您必须进一步更改对齐方式才能达到边界,但您可以达到边界,因为它是 2 或 4 个时钟加上总线开销以增加额外费用fetch 你可以看到那些。
如果我放两个 nops
nop
nop
.thumb_func
.globl TEST
TEST:
给
08000114 <inner>:
8000114: f000 f874 bl 8000200 <soft_delay>
8000118: 3d01 subs r5, #1
800011a: d1fb bne.n 8000114 <inner>
800011c: 6823 ldr r3, [r4, #0]
800011e: 1af0 subs r0, r6, r3
8000120: bd70 pop {r4, r5, r6, pc}
给
00001B59
所以很好,我们回到了那个数字,可以再试几次来确认,但看起来对齐对我们的外部测试循环很敏感,这很糟糕,但我们可以管理,不要改变它不会影响考试。 如果我不关心对齐并且有这样的事情:
void soft_delay(void) {
for (volatile unsigned int i=0; i<0x2000; ++i) { }
}
int notmain ( void )
{
unsigned int ra;
unsigned int beg;
unsigned int end;
PUT32(STK_CSR,4);
PUT32(STK_RVR,0x00FFFFFF);
PUT32(STK_CVR,0x00000000);
PUT32(STK_CSR,5);
uart_init();
hexstring(0x12345678);
beg=GET32(STK_CVR);
for(ra=0;ra<1000;ra++)
{
soft_delay();
}
end=GET32(STK_CVR);
hexstring((beg-end)&0x00FFFFFF);
return(0);
}
然后,当我使用优化选项并使用不同的编译器时,测试循环前面的程序/二进制文件中的任何更改都会/可能会移动测试循环,从而改变其性能,在我的简单示例中,性能差异为 14% ,如果您正在进行性能测试,那将是巨大的。 让编译器处理所有这一切,而我们无法控制被测函数前面的所有内容可能会干扰被测函数,如上文所述,编译器可能会选择内联该函数而不是调用它,这会变得更有趣情况就像测试循环一样,虽然可能不像我的那么干净,当然如果没有优化的话肯定不会,但是现在被测试的代码随着选项或对齐方式的变化是动态的。
我很高兴你碰巧使用了这个核心/芯片......
如果我重新调整内部,现在搞砸了
.align 8
nop
soft_delay:
bx lr
08000202 <soft_delay>:
8000202: 4770 bx lr
这是一条指令,从我们所读到的 0x200 处提取,似乎能够分辨出来。 没想到这会改变任何事情,它也没有
00001B59
但是现在我们知道了我们所知道的,我们可以利用我们的经验来处理这个琐碎的一点都不有趣的例子。
.align 8
nop
soft_delay:
nop
bx lr
给
00001F41
正如预期的那样。 我们可以享受更多乐趣:
.align 8
.word 0,0
nop
.thumb_func
.globl TEST
TEST:
联合给予
08000112 <inner>:
8000112: f000 f876 bl 8000202 <soft_delay>
8000116: 3d01 subs r5, #1
8000118: d1fb bne.n 8000112 <inner>
08000202 <soft_delay>:
8000202: 46c0 nop ; (mov r8, r8)
8000204: 4770 bx lr
如果您知道自己在做什么,那就不足为奇了:
00002328
9000 个时钟,29% 的性能差异。 我们实际上是在谈论 5 条(技术上为 6 条)指令,完全相同的机器代码,并且通过简单地改变对齐方式,性能可以有 29% 的不同,编译器和选项与它无关,然而,甚至还没有达到。
我们怎么能期望使用循环方法中的代码多次来对程序进行任何类型的性能评估? 我们不能,除非我们知道我们在做什么,了解架构等。
现在应该很明显并且阅读文档我正在使用内部 8Mhz 时钟,所有内容都来自于此,因此系统时间有时不会像您在 dram 中看到的那样发生变化。 对于 0 < SYSCLK <- 24Mhz,FLASH_ACR 寄存器中的 LATENCY 位应默认为零等待状态。 如果我将时钟提高到 24Mhz 以上,处理器运行得更快,但现在闪存相对于处理器更慢。
无需弄乱时钟,只需将 FLASH_ACR 寄存器更改为 0x31 即可添加等待状态。
000032C6
12998 从 9000 上升,我没想到它一定会翻倍,但事实并非如此。
嗯,为了好玩,使用 strh 制作一个 PUT16,然后
.thumb_func
.globl HOP
HOP:
bx r2
和
PUT16(0x2000010a,0xb570); // 800010a: b570 push {r4, r5, r6, lr}
PUT16(0x2000010c,0x1c04); // 800010c: 1c04 adds r4, r0, #0
PUT16(0x2000010e,0x1c0d); // 800010e: 1c0d adds r5, r1, #0
PUT16(0x20000110,0x6826); // 8000110: 6826 ldr r6, [r4, #0]
PUT16(0x20000112,0xf000); // 8000112: f000 f876 bl 8000202 <soft_delay>
PUT16(0x20000114,0xf876); // 8000112: f000 f876 bl 8000202 <soft_delay>
PUT16(0x20000116,0x3d01); // 8000116: 3d01 subs r5, #1
PUT16(0x20000118,0xd1fb); // 8000118: d1fb bne.n 8000112 <inner>
PUT16(0x2000011a,0x6823); // 800011a: 6823 ldr r3, [r4, #0]
PUT16(0x2000011c,0x1af0); // 800011c: 1af0 subs r0, r6, r3
PUT16(0x2000011e,0xbd70); // 800011e: bd70 pop {r4, r5, r6, pc}
PUT16(0x20000202,0x46c0); // 8000202: 46c0 nop ; (mov r8, r8)
PUT16(0x20000204,0x4770); // 8000204: 4770 bx lr
hexstring(HOP(STK_CVR,1000,0x2000010B));
给出 0000464B
这完全出乎意料。 但基本上是18,000
在这之后让公羊上床睡觉
PUT16(0x20000108,0xb570); // 800010a: b570 push {r4, r5, r6, lr}
PUT16(0x2000010a,0x1c04); // 800010c: 1c04 adds r4, r0, #0
PUT16(0x2000010c,0x1c0d); // 800010e: 1c0d adds r5, r1, #0
PUT16(0x2000010e,0x6826); // 8000110: 6826 ldr r6, [r4, #0]
PUT16(0x20000110,0xf000); // 8000112: f000 f876 bl 8000202 <soft_delay>
PUT16(0x20000112,0xf876); // 8000112: f000 f876 bl 8000202 <soft_delay>
PUT16(0x20000114,0x3d01); // 8000116: 3d01 subs r5, #1
PUT16(0x20000116,0xd1fb); // 8000118: d1fb bne.n 8000112 <inner>
PUT16(0x20000118,0x6823); // 800011a: 6823 ldr r3, [r4, #0]
PUT16(0x2000011a,0x1af0); // 800011c: 1af0 subs r0, r6, r3
PUT16(0x2000011c,0xbd70); // 800011e: bd70 pop {r4, r5, r6, pc}
PUT16(0x20000200,0x46c0); // 8000202: 46c0 nop ; (mov r8, r8)
PUT16(0x20000200,0x4770); // 8000204: 4770 bx lr
hexstring(HOP(STK_CVR,1000,0x20000109));
00002EDE
机器码没有改变,因为我将两者都向后移动了 2,因此它们之间的相对地址是相同的。 请注意, bl 是两条单独的指令,而不是一条 32 位指令。 您无法在较新的文档中看到这一点,您需要返回到解释它的原始/早期 ARM ARM。 并且很容易进行实验,将两条指令分开并在两者之间放入其他东西,它们工作得很好,因为它们是两个单独的指令。
在这一点上,读者应该能够制作一个 2 指令测试循环,计时并使用完全相同的机器代码显着改变在该平台上执行这两条指令的性能。
因此,让我们尝试您编写的 volatile 循环。
.align 8
soft_delay:
push {r7, lr}
sub sp, #8
add r7, sp, #0
mov r3, #0
str r3, [r7, #4]
b L12
Lc:
ldr r3, [r7, #4]
add r3, #1
str r3, [r7, #4]
L12:
ldr r3, [r7, #4]
ldr r2, L24
cmp r3, r2
bls Lc
nop
mov sp, r7
add sp, #8
pop {r7, pc}
nop
.align
L24: .word 0x1FFF
这是我相信未优化的 -O0 版本。 从一个测试循环开始
hexstring(TEST(STK_CVR,1));
经验,我们看到的次数会溢出我们的 24 位计数器,结果将非常奇怪或导致错误的结论。
0001801F
98,000,快速检查安全:
.align
L24: .word 0x1F
0000019F
不错,与 256 倍的速度相当。
所以我们在测试循环中有一些回旋余地,但没有太多尝试 10
hexstring(TEST(STK_CVR,10));
000F012D
每个循环 98334 个滴答声。
改变对齐方式
08000202 <soft_delay>:
8000202: b580 push {r7, lr}
8000204: b082 sub sp, #8
给出了相同的结果
000F012D
并非闻所未闻,如果您想通过每个指令检查获取周期等进行计数,您可以检查差异。
我做了测试:
soft_delay:
nop
nop
bx lr
它的两个获取周期无论对齐方式如何,或者如果我像我们看到的那样将 bx lr 留在没有 nops 的情况下,只需在测试中使用奇数个指令,然后对齐就不会影响获取的结果,但请注意根据我们现在所知道的,程序中有一些其他代码移动了可能改变了性能的外部计时/测试循环,结果可能会显示两个测试之间的差异,这两个测试纯粹是计时代码而不是被测代码(阅读 Michael Abrash )。
cortex-m3 基于 armv7-m 架构。 如果我将编译器从 -mcpu=cortex-m0(到目前为止所有 cortex-m 兼容)更改为 -mcpu=cortex-m3(并非所有 cortex-m 兼容都会在其中的一半上中断),它会生成更少的代码。
.align 8
soft_delay:
push {r7}
sub sp, #12
add r7, sp, #0
movs r3, #0
str r3, [r7, #4]
b L12
Lc:
ldr r3, [r7, #4]
add r3, #1
str r3, [r7, #4]
L12:
ldr r3, [r7, #4]
/*14: f5b3 5f00 cmp.w r3, #8192 ; 0x2000*/
//cmp.w r3, #8192
.word 0x5f00f5b3
bcc Lc
nop
add r7, #12
mov sp, r7
pop {r7}
bx lr
000C80FB 81945 被测代码的滴答声。
我讨厌统一语法,这是一个巨大的错误,所以我在遗留模式下摸索。 因此 .word 东西在中间。
作为写这篇文章的一部分,我有点搞砸了我的系统,以展示一些东西。 我正在构建一个 gcc 5.4.0 但覆盖了我的 9.2.0 所以不得不重新构建两者。
2.95 是我开始使用 arm 的版本,不支持拇指 gcc 3.xx 是第一个。 并且 gcc 4.xx 或 gcc 5.xx 为我的一些项目生成了“较慢”的代码,在工作中,我们目前正在为我们的构建系统从 ubuntu 16.04 迁移到 18.04,如果您使用 apt-got 交叉编译器进行 arm将您从 5.xx 移动到 7.xx 并且它正在为相同的源代码制作更大的二进制文件,并且在我们内存紧张的情况下,它使我们超出了可用的范围,因此我们必须删除一些代码(最容易使打印的消息更短,剪掉文本)或通过构建我们自己的或 apt-getting 旧编译器来坚持使用旧编译器。 19.10 不再提供 5.xx 版本。
所以现在两者都建好了。
18: d3f8 bcc.n c <soft_delay+0xc>
1a: bf00 nop
1c: bf00 nop
1e: 370c adds r7, #12
bcc 之后的这些 nops 令我感到困惑......
18: d3f8 bcc.n c <soft_delay+0xc>
1a: bf00 nop
1c: 370c adds r7, #12
gcc 5.4.0 放了一个,gcc 9.2.0 放了两个 nops,ARM 没有 MIPS 的分支影子(MIPS 目前也没有)。
000C80FB gcc 5.4.0
000C8105 gcc 9.2.0
我调用该函数 10 次,nop 位于测试循环之外的代码,因此影响较小。
使用 gcc 9.2.0 优化了所有 cortex-m 变体(迄今为止)
soft_delay:
mov r3, #0
mov r2, #128
sub sp, #8
str r3, [sp, #4]
ldr r3, [sp, #4]
lsl r2, r2, #6
cmp r3, r2
bcs L1c
L10:
ldr r3, [sp, #4]
add r3, #1
str r3, [sp, #4]
ldr r3, [sp, #4]
cmp r3, r2
bcc L10
L1c:
add sp, #8
bx lr
(还要明白,当您构建编译器时,并非所有人都说 gcc 9.2.0 构建生成相同的代码,您有选项,这些选项会影响输出,使 9.2.0 的不同构建可能产生不同的结果)
000C80B5
为 cortex-m3 构建的 gcc 9.2.0:
soft_delay:
mov r3, #0
sub sp, #8
str r3, [sp, #4]
ldr r3, [sp, #4]
/*8: f5b3 5f00 cmp.w r3, #8192 ; 0x2000*/
.word 0x5F00F5B3
bcs L1c
Le:
ldr r3, [sp, #4]
add r3, #1
str r3, [sp, #4]
ldr r3, [sp, #4]
/*16: f5b3 5f00 cmp.w r3, #8192 ; 0x2000*/
.word 0x5F00F5B3
bcc Le
L1c:
add sp, #8
bx lr
000C80A1
那是在噪音中。 尽管构建的代码有差异。 他们只是没有在更少的指令中比较 0x2000 获得收益。 并注意,如果您将 0x2000 更改为其他数字,那么这不仅会使循环花费更长的时间,还可以更改此类架构的生成代码。
我喜欢制作这些计数延迟循环的方式是使用编译域之外的函数
extern void dummy ( unsigned int );
void soft_delay(void) {
for (unsigned int i=0; i<0x2000; ++i) { dummy(i); }
}
soft_delay:
push {r4, r5, r6, lr}
mov r5, #128
mov r4, #0
lsl r5, r5, #6
L8:
mov r0, r4
add r4, #1
bl dummy
cmp r4, r5
bne L8
pop {r4, r5, r6, pc}
那里的功能是您不需要调用什么 volatile 的开销,并且显然由于调用也存在开销,但没有那么多
000B40C9
甚至更好:
soft_delay:
sub r0,#1
bne soft_delay
bx lr
我必须更改包裹在被测代码周围的代码才能使该功能正常工作。
另一个特定于这些目标的注意事项,但也是您要处理的事项
unsigned int more_fun ( unsigned int, unsigned int );
unsigned int fun ( unsigned int a, unsigned int b )
{
return(more_fun(a,b)+a+(b<<2));
}
00000000 <fun>:
0: b570 push {r4, r5, r6, lr}
2: 000c movs r4, r1
4: 0005 movs r5, r0
6: f7ff fffe bl 0 <more_fun>
a: 00a4 lsls r4, r4, #2
c: 1964 adds r4, r4, r5
e: 1820 adds r0, r4, r0
10: bd70 pop {r4, r5, r6, pc}
12: 46c0 nop ; (mov r8, r8)
一个问题在 SO 上定期重复。 为什么它推动 r6 它没有使用 r6。
编译器使用我所说的并且曾经被称为调用约定来运行,现在他们使用术语 ABI、EABI,无论哪种情况,它都是同一件事,它是编译器针对特定目标遵循的一组规则。 Arm 添加了一个规则来保持堆栈在 64 位地址边界而不是 32 位地址边界上对齐,这导致额外的项目保持堆栈对齐,在那里使用的寄存器可能会有所不同。 如果您使用较旧的 gcc 与较新的 gcc,这会/会影响您的代码的性能。
这里有很多因素在起作用。 当然,如果您有一个优化编译器,并且比较优化与不依赖于代码,您会发现执行速度有很大差异。 在这里的小循环中使用 volatile 实际上掩盖了其中的一些,在这两种情况下,它应该在每个循环中读/写到内存中。
但是循环变量未优化的调用代码将在该循环中接触 ram 两到三次,理想情况下将一直在寄存器中进行优化,即使在零等待状态 ram 的情况下,执行性能也会产生显着差异。
切换引脚代码相对较大(直接与外设交谈会减少代码),这取决于该库是使用不同选项单独编译还是同时使用相同选项编译,这对性能有很大的影响。
补充一点,这是一个 mcu 并且正在运行闪存,随着这部分的老化,闪存最多可能是 CPU 时钟速率的一半,最糟糕的是一些等待状态,如果 ST 有的话,我不记得了当时它前面的缓存。 所以你添加的每条指令都可以添加一个时钟,所以仅循环变量就可以显着改变时序。
作为一个高性能流水线核心,我已经在这里和其他地方证明了对齐可以(并不总是)发挥作用,所以如果在一种情况下完全相同的机器代码链接到地址 0x100 和 0x102 在另一种情况下,可能完全相同根据设计中预取器的性质或闪存实现、缓存(如果有实现)、分支预测器等,机器代码需要额外或更少的时钟来执行。
然后最大的问题是你是如何计时的,不正确使用时钟的错误并不少见,这样时钟/时序代码本身就会发生变化,并产生一些差异。 另外,是否有背景事情发生,中断/多任务处理。
Michal Abrash 写了一本很棒的书,名为《汇编语言之禅》,您可以在 ePub 或 GitHub 上的 pdf 格式中免费获得它。 这本书出版时 8088 已经过时了,但如果你专注于此,那么你就错过了重点,我在它出版时买了它,并且几乎每天都在使用我学到的东西。
gcc 不是一个高性能编译器,它更像是一个构建 Unix 风格的通用编译器,您可以在其中拥有不同的语言前端和不同的目标后端。 当我处于您现在试图首先了解这些事情的位置时,我为相同的 arm 目标和相同的 C 代码对许多编译器进行了采样,结果非常庞大。 后来我写了一个指令集模拟器,所以我可以计算指令和内存访问来比较 gnu 和 llvm,因为后者比 gnu 有更多的优化机会,但对于代码的执行测试,gcc 有时更快但不慢。 与我用来分析差异的事情相比,这最终更像是一个长周末的乐趣。
从像这样的小代码开始并反汇编两者更容易。 了解更少的指令并不意味着更快,基于 dram 的系统上的一个远程内存访问可能需要数百个时钟周期,这可能会被另一种解决方案所取代,该解决方案需要少数/数十条线性提取指令才能最终得到相同的结果(做一些数学运算与在很少采样的表中查找某些内容)并且根据情况,十几个指令执行得更快。 同时,表解决方案可以快得多。 这取决于。
对反汇编的检查通常会导致错误的结论(粗略阅读,不仅仅是那本书,所有内容),因为人们首先认为更少的指令意味着更快的代码。 如果将指令移动到本来会浪费时钟的时间段,则在流水线处理器中重新排列指令可以提高性能。 在内存访问之前而不是在非超缩放处理器之后递增与内存访问无关的寄存器。
啊,回到评论。 这是几年前的事情,大多数人只是将他们的 gui 包装在 gnu 周围,而竞争编译器更多的是一种东西,而 ide/gui 是产品而不是编译器。 但是有 arm 编译器本身,在 rvct 工具、广告和我忘记了其他之前,那些比 gcc “更好”。 我忘记了其他人的名字,但有一个产生明显更快的代码,当然这是 Dhrystone,所以你还会发现他们可能会为 Dhrystone 调整优化器,只是为了玩基准游戏。 现在我可以看到操纵基准是多么容易,我认为它们通常是 bu33zzit,不可信。 Kiel 曾经是 mcus 和类似的多目标工具,但后来被 arm 购买了,我当时认为他们正在放弃所有其他目标,但有一段时间没有检查。 我可能已经尝试过一次以获得 rvct 的免费/演示版本,因为当我在一份工作中工作时,我们有预算购买数千美元的工具,但这不包括 rvct(尽管我在打电话与以前的 Allant 人员通话,他们参与了购买成为 rvct 工具),一旦他们完成了该产品的开发/集成,我就很想尝试,那时没有预算,后来又不能”甚至连 kiels 工具都买不起或不感兴趣,更不用说武器了。 他们早期的 rvct 演示创建了一个加密/混淆的二进制文件,它不是 arm 机器代码,它只能在他们的模拟器上运行,所以你不能用它来评估性能或与其他人进行比较,不要认为他们愿意给我们一个未混淆的版本,我们不愿意对其进行逆向工程。 现在只需使用 gcc 或 clang 并在需要的地方手动优化就更容易了。 同样,有经验的人可以根据检查编译器输出的经验编写优化更好的 C 代码。
您必须了解硬件,特别是在这种情况下,您使用处理器 IP,并且大部分芯片与处理器 IP 无关,并且大部分性能与处理器 IP 无关(对于当今的许多平台来说几乎是正确的)特别是您的服务器/台式机/笔记本电脑)。 例如,Gameboy Advance 使用了大量 16 位总线而不是 32 位总线,拇指工具几乎没有被集成,但拇指模式在计算指令/或字节数时就像当时多 10% 的代码,在该芯片上执行速度明显更快。 在其他实现中,arm 架构和芯片设计拇指可能有或没有性能损失。
通常,带有 cortex-m 产品的 ST 倾向于在闪存前面放置一个缓存,有时他们会记录它并提供启用/禁用控制有时不会,因此充其量很难获得真正的性能值,因为典型的事情是在循环中多次运行被测代码,以便获得更好的时间测量。 其他供应商不一定会这样做,而且更容易查看闪存等待状态并获得可用于验证设计的真实、最坏情况下的时序值。 一般的缓存以及管道使得很难获得好的、可重复的、可靠的数字来验证您的设计。 因此,例如,您有时无法使用对齐技巧来干扰 st 上相同机器代码的性能,但可以说具有相同内核的 ti。 st 可能不会在 cortex-m7 中为您提供 icache,而其他供应商可能会因为 st 已经涵盖了这一点。 即使在一个品牌名称内,也不要指望一个芯片/系列的结果转化为另一个芯片/系列,即使它们使用相同的内核。 还要查看 arm 文档中关于某些内核是否提供获取大小作为广告选项、单周期或多周期乘法等的微妙评论,我会告诉您内核还有其他编译时间选项技术参考手册中未显示会影响性能的信息,因此不要假设所有 cortex-m3 都是相同的,即使它们来自 arm 的相同修订版。 芯片供应商拥有源代码,因此他们可以进一步修改它,或者例如由消费者实现的寄存器组,他们可能会将其从无保护更改为奇偶校验到 ecc,这可能会影响性能,同时保留所有武器的原始代码照原样。 当您查看 avr 或 pic 甚至是 msp430 时,虽然我无法证明这些设计看起来更静态,但与 Xmega 相比,与常规旧 avr 相比,这些设计显得更加静态,因为它们之间存在明显的差异,但彼此之间却很小。
您的假设是一个好的开始,确实没有过度优化之类的事情,更多的是错过优化的事情,但是您的假设中可能没有看到的其他因素可能会或可能不会在拆卸。 有一些显而易见的事情,我们希望循环变量之一是基于寄存器的还是基于内存的。 对齐,如果您使用相同的代码,我不希望时钟设置发生变化,但是使用计时器或示波器进行一组不同的实验,您可以测量时钟设置以查看它们的配置是否相同。 后台任务、中断和关于他们如何/何时通过测试的愚蠢运气。 但最重要的是,有时它就像错过了优化或一个编译器如何为另一个编译器生成代码的细微差异一样简单,但通常不是这些问题,更多的是系统问题、内存速度、外围速度、缓存或其架构,设计中的各种总线如何运行等。对于这些 cortex-ms(和许多其他处理器)中的一些,您可以利用它们的总线行为来显示普通人不希望看到的性能差异。
Keil过度优化(不太可能,因为代码很简单)
你不能过度优化你可能会不足/错过的东西,所以如果有任何 gcc 错过了 Kiel 没有的东西。 不是相反
由于错误的编译器标志,arm-none-eabi-gcc 优化不足(我使用 CLion Embedded plugins` CMakeLists.txt)
将在下面看到,但这很可能是 esp 调试与发布,我从不为调试而构建(从不使用调试器),您必须对所有内容进行两次测试,如果您不进行测试,则调试会变得更加困难,因此发布版本如果有问题需要更多的工作来找出问题。
初始化中的一个错误,使芯片具有较低的时钟频率与 arm-none-eabi-gcc(有待调查)
我的猜测不是这个,这意味着你犯了一个非常大的错误并且没有在每个工具上编译相同的代码,所以这不是一个公平的比较。
让我们运行它。
使用 systick 计时器,24 位(在 r0 中传递的当前值寄存器地址)
.align 8
.thumb_func
.globl TEST
TEST:
push {r4,r5,r6,lr}
mov r4,r0
ldr r5,[r4]
bl soft_delay
ldr r3,[r4]
sub r0,r5,r3
pop {r4,r5,r6,pc}
为避免 24 位定时器溢出,循环计数限制为 200000 次而不是 2000000 次。 我假设您遗漏的代码是 2000000 - 1。如果不是,这仍然显示相关差异。
-O0 代码
.align 8
soft_delay:
PUSH {r3,lr}
MOV r0,#0
STR r0,[sp,#0]
B L6.14
L6.8:
LDR r0,[sp,#0]
ADD r0,r0,#1
STR r0,[sp,#0]
L6.14:
LDR r1,L6.24
LDR r0,[sp,#0]
CMP r0,r1
BCC L6.8
POP {r3,pc}
.align
L6.24: .word 100000 - 1
08000200 <soft_delay>:
8000200: b508 push {r3, lr}
8000202: 2000 movs r0, #0
8000204: 9000 str r0, [sp, #0]
8000206: e002 b.n 800020e <L6.14>
08000208 <L6.8>:
8000208: 9800 ldr r0, [sp, #0]
800020a: 3001 adds r0, #1
800020c: 9000 str r0, [sp, #0]
0800020e <L6.14>:
800020e: 4902 ldr r1, [pc, #8] ; (8000218 <L6.24>)
8000210: 9800 ldr r0, [sp, #0]
8000212: 4288 cmp r0, r1
8000214: d3f8 bcc.n 8000208 <L6.8>
8000216: bd08 pop {r3, pc}
08000218 <L6.24>:
8000218: 0001869f
00124F8B systick timer ticks
-O1 代码
soft_delay:
PUSH {r3,lr}
MOV r0,#0
STR r0,[sp,#0]
LDR r0,L6.24
B L6.16
L6.10:
LDR r1,[sp,#0]
ADD r1,r1,#1
STR r1,[sp,#0]
L6.16:
LDR r1,[sp,#0]
CMP r1,r0
BCC L6.10
POP {r3,pc}
.align
L6.24: .word 100000 - 1
08000200 <soft_delay>:
8000200: b508 push {r3, lr}
8000202: 2000 movs r0, #0
8000204: 9000 str r0, [sp, #0]
8000206: 4804 ldr r0, [pc, #16] ; (8000218 <L6.24>)
8000208: e002 b.n 8000210 <L6.16>
0800020a <L6.10>:
800020a: 9900 ldr r1, [sp, #0]
800020c: 3101 adds r1, #1
800020e: 9100 str r1, [sp, #0]
08000210 <L6.16>:
8000210: 9900 ldr r1, [sp, #0]
8000212: 4281 cmp r1, r0
8000214: d3f9 bcc.n 800020a <L6.10>
8000216: bd08 pop {r3, pc}
08000218 <L6.24>:
8000218: 0001869f
000F424E systicks
-O2 代码
soft_delay:
SUB sp,sp,#4
MOVS r0,#0
STR r0,[sp,#0]
LDR r0,L4.24
L4.8:
LDR r1,[sp,#0]
ADDS r1,r1,#1
STR r1,[sp,#0]
CMP r1,r0
BCC L4.8
ADD sp,sp,#4
BX lr
.align
L4.24: .word 100000 - 1
08000200 <soft_delay>:
8000200: b081 sub sp, #4
8000202: 2000 movs r0, #0
8000204: 9000 str r0, [sp, #0]
8000206: 4804 ldr r0, [pc, #16] ; (8000218 <L4.24>)
08000208 <L4.8>:
8000208: 9900 ldr r1, [sp, #0]
800020a: 3101 adds r1, #1
800020c: 9100 str r1, [sp, #0]
800020e: 4281 cmp r1, r0
8000210: d3fa bcc.n 8000208 <L4.8>
8000212: b001 add sp, #4
8000214: 4770 bx lr
8000216: 46c0 nop ; (mov r8, r8)
08000218 <L4.24>:
8000218: 0001869f
000AAE65 systicks
-O3
soft_delay:
PUSH {r3,lr}
MOV r0,#0
STR r0,[sp,#0]
LDR r0,L5.20
L5.8:
LDR r1,[sp,#0]
ADD r1,r1,#1
STR r1,[sp,#0]
CMP r1,r0
BCC L5.8
POP {r3,pc}
.align
L5.20: .word 100000 - 1
08000200 <soft_delay>:
8000200: b508 push {r3, lr}
8000202: 2000 movs r0, #0
8000204: 9000 str r0, [sp, #0]
8000206: 4803 ldr r0, [pc, #12] ; (8000214 <L5.20>)
08000208 <L5.8>:
8000208: 9900 ldr r1, [sp, #0]
800020a: 3101 adds r1, #1
800020c: 9100 str r1, [sp, #0]
800020e: 4281 cmp r1, r0
8000210: d3fa bcc.n 8000208 <L5.8>
8000212: bd08 pop {r3, pc}
08000214 <L5.20>:
8000214: 0001869f
000AAE6A systicks
有趣的是,对齐不会影响任何这些结果。
在电子表格中比较彼此的结果和上述结果
18.7 1.000 00124F8B 1200011 1.000
13.3 0.711 000F424E 1000014 0.833
9.8 0.524 000AAE65 700005 0.583
9.9 0.529 000AAE6A 700010 0.583
它表明,我测量的各个阶段也显示出改进,并且 -O3 稍慢。
分析发生了什么。
void soft_delay(void) {
for (volatile uint32_t i=0; i<2000000; ++i) { }
}
因为这会向上计数并且是易失性的,所以编译器无法进行通常的向下计数并保存指令(subs then bne 而不是 add、cmp、bcc)
-O0 代码
soft_delay:
PUSH {r3,lr} allocate space for i
MOV r0,#0 i = 0
STR r0,[sp,#0] i = 0
B L6.14
L6.8:
LDR r0,[sp,#0] read i from memory
ADD r0,r0,#1 increment i
STR r0,[sp,#0] save i to memory
L6.14:
LDR r1,L6.24 read max value
LDR r0,[sp,#0] read i from memory
CMP r0,r1 compare i and max value
BCC L6.8 branch if unsigned lower
POP {r3,pc} return
我应该首先检查代码 L6.24 应该是 2000000 而不是 2000000 - 1。你没有考虑这个问题。
没有优化通常意味着像在高级语言中一样按顺序敲出代码。
r3 不需要保留,LR 也不需要保留,但变量是易失性的,因此它需要堆栈上的空间,编译器选择以这种方式执行此优化级别,推 lr 允许它在最后弹出 pc。
push 是 stm (stmdb) 的伪指令,因此从堆栈指针中减去 8,然后按顺序保存寄存器,因此如果 sp 位于 0x1008,则它更改为 0x1000 并将 r3 写入 0x1000,将 lr 写入 0x1004,其余部分在这个函数中,它使用 sp+0,在这个例子中是 0x1000。 这种方式使用的r3和push就是为代码中的变量i分配一个位置。
-O1 版本
soft_delay:
PUSH {r3,lr} allocate space
MOV r0,#0 i = 0
STR r0,[sp,#0] i = 0
LDR r0,L6.24 read max/test value
B L6.16
L6.10:
LDR r1,[sp,#0] load i from memory
ADD r1,r1,#1 increment i
STR r1,[sp,#0] save i to memory
L6.16:
LDR r1,[sp,#0] read i from memory
CMP r1,r0 compare i with test value
BCC L6.10 branch if unsigned lower
POP {r3,pc}
在这种情况下 -O0 和 -O1 之间的主要区别是 -O0 版本每次通过循环读取最大值。 -O1 版本在循环外读取它一次。
-O0
08000208 <L6.8>:
8000208: 9800 ldr r0, [sp, #0]
800020a: 3001 adds r0, #1
800020c: 9000 str r0, [sp, #0]
800020e: 4902 ldr r1, [pc, #8] ; (8000218 <L6.24>)
8000210: 9800 ldr r0, [sp, #0]
8000212: 4288 cmp r0, r1
8000214: d3f8 bcc.n 8000208 <L6.8>
1200011 / 100000 = 12
大部分时间都在上述循环中。 7条指令三个加载两个存储。 那是 12 件事,所以也许每个时钟一个。
-O1 代码
0800020a <L6.10>:
800020a: 9900 ldr r1, [sp, #0]
800020c: 3101 adds r1, #1
800020e: 9100 str r1, [sp, #0]
08000210 <L6.16>:
8000210: 9900 ldr r1, [sp, #0]
8000212: 4281 cmp r1, r0
8000214: d3f9 bcc.n 800020a <L6.10>
1000014 / 100000 = 10
0800020a <L6.10>:
800020a: 9900 ldr r1, [sp, #0]
800020c: 3101 adds r1, #1
800020e: 9100 str r1, [sp, #0]
8000210: 9900 ldr r1, [sp, #0]
8000212: 4281 cmp r1, r0
8000214: d3f9 bcc.n 800020a <L6.10>
6条指令,两载一存。 8 件事 10 个时钟。 这里与 -O0 的区别在于比较值是在循环之前/之外读取的,以便保存该指令和该内存周期。
-O2 代码
08000208 <L4.8>:
8000208: 9900 ldr r1, [sp, #0]
800020a: 3101 adds r1, #1
800020c: 9100 str r1, [sp, #0]
800020e: 4281 cmp r1, r0
8000210: d3fa bcc.n 8000208 <L4.8>
700005 / 100000 = 每个循环 7 滴答
所以根据某些人的定义,这不是在尊重易变者,是吗? 比较值在循环之外,它的写入方式应该是 2000000 + 1,是吗? 它每次循环从内存中读取 i 一次而不是两次,但每次通过循环都会使用新值存储它。 基本上它删除了第二次加载并节省了一些等待读取完成的时间。
-O3 代码
08000208 <L5.8>:
8000208: 9900 ldr r1, [sp, #0]
800020a: 3101 adds r1, #1
800020c: 9100 str r1, [sp, #0]
800020e: 4281 cmp r1, r0
8000210: d3fa bcc.n 8000208 <L5.8>
内循环与-O2 相同。
-O2 这样做
08000200 <soft_delay>:
8000200: b081 sub sp, #4
8000202: 2000 movs r0, #0
8000204: 9000 str r0, [sp, #0]
8000206: 4804 ldr r0, [pc, #16] ; (8000218 <L4.24>)
...
8000212: b001 add sp, #4
8000214: 4770 bx lr
-O3 这样做
08000200 <soft_delay>:
8000200: b508 push {r3, lr}
8000202: 2000 movs r0, #0
8000204: 9000 str r0, [sp, #0]
8000206: 4803 ldr r0, [pc, #12] ; (8000214 <L5.20>)
8000212: bd08 pop {r3, pc}
现在指令更少了,但是推送和弹出需要更长的时间,它们具有内存周期开销,即使指令较少,堆栈指针指令的减法和加法也比那些内存周期快。 所以时间上的细微差别是循环外的 push/pop。
现在为 GCC (9.2.0)
对于初学者,我不知道 Kiel 是否针对拇指一般(所有变体)专门针对 cortex-ms 或 cortex-m3。
第一个 -O0 代码:
-O0
soft_delay:
push {r7, lr}
sub sp, sp, #8
add r7, sp, #0
movs r3, #0
str r3, [r7, #4]
b .L2
.L3:
ldr r3, [r7, #4]
adds r3, r3, #1
str r3, [r7, #4]
.L2:
ldr r3, [r7, #4]
ldr r2, .L4
cmp r3, r2
bls .L3
nop
nop
mov sp, r7
add sp, sp, #8
@ sp needed
pop {r7}
pop {r0}
bx r0
.L5:
.align 2
.L4:
.word 199999
08000200 <soft_delay>:
8000200: b580 push {r7, lr}
8000202: b082 sub sp, #8
8000204: af00 add r7, sp, #0
8000206: 2300 movs r3, #0
8000208: 607b str r3, [r7, #4]
800020a: e002 b.n 8000212 <soft_delay+0x12>
800020c: 687b ldr r3, [r7, #4]
800020e: 3301 adds r3, #1
8000210: 607b str r3, [r7, #4]
8000212: 687b ldr r3, [r7, #4]
8000214: 4a04 ldr r2, [pc, #16] ; (8000228 <soft_delay+0x28>)
8000216: 4293 cmp r3, r2
8000218: d9f8 bls.n 800020c <soft_delay+0xc>
800021a: 46c0 nop ; (mov r8, r8)
800021c: 46c0 nop ; (mov r8, r8)
800021e: 46bd mov sp, r7
8000220: b002 add sp, #8
8000222: bc80 pop {r7}
8000224: bc01 pop {r0}
8000226: 4700 bx r0
8000228: 00030d3f andeq r0, r3, pc, lsr sp
00124F9F
我们立即看到两件事,首先是 Kiel 没有构建的堆栈框架,其次是比较后的这些神秘 nop,一定是一些芯片勘误表或其他东西,需要查一下。 从我现在可能删除的另一个答案中,gcc 5.4.0 放了一个 nop,tcc 9.2.0 放了两个。 所以这个循环有
1200031 / 100000 = 每个循环 12 个滴答
800020c: 687b ldr r3, [r7, #4]
800020e: 3301 adds r3, #1
8000210: 607b str r3, [r7, #4]
8000212: 687b ldr r3, [r7, #4]
8000214: 4a04 ldr r2, [pc, #16] ; (8000228 <soft_delay+0x28>)
8000216: 4293 cmp r3, r2
8000218: d9f8 bls.n 800020c <soft_delay+0xc>
这段代码花费时间的主循环也是 12 个滴答,就像 Kiel 一样,只是不同的寄存器无关紧要。 微妙的整体时间差异是堆栈帧和额外的 nops 使 gcc 版本稍长。
arm-none-eabi-gcc -O0 -fomit-frame-pointer -c -mthumb -mcpu=cortex-m0 hello.c -o hello.o
arm-none-eabi-objdump -D hello.o > hello.list
arm-none-eabi-gcc -O0 -fomit-frame-pointer -S -mthumb -mcpu=cortex-m0 hello.c
如果我在没有帧指针的情况下构建,则 gcc -O0 变为
soft_delay:
sub sp, sp, #8
movs r3, #0
str r3, [sp, #4]
b .L2
.L3:
ldr r3, [sp, #4]
adds r3, r3, #1
str r3, [sp, #4]
.L2:
ldr r3, [sp, #4]
ldr r2, .L4
cmp r3, r2
bls .L3
nop
nop
add sp, sp, #8
bx lr
.L5:
.align 2
.L4:
.word 99999
08000200 <soft_delay>:
8000200: b082 sub sp, #8
8000202: 2300 movs r3, #0
8000204: 9301 str r3, [sp, #4]
8000206: e002 b.n 800020e <soft_delay+0xe>
8000208: 9b01 ldr r3, [sp, #4]
800020a: 3301 adds r3, #1
800020c: 9301 str r3, [sp, #4]
800020e: 9b01 ldr r3, [sp, #4]
8000210: 4a03 ldr r2, [pc, #12] ; (8000220 <soft_delay+0x20>)
8000212: 4293 cmp r3, r2
8000214: d9f8 bls.n 8000208 <soft_delay+0x8>
8000216: 46c0 nop ; (mov r8, r8)
8000218: 46c0 nop ; (mov r8, r8)
800021a: b002 add sp, #8
800021c: 4770 bx lr
800021e: 46c0 nop ; (mov r8, r8)
8000220: 0001869f
00124F94
并且比 Kiel 的其他 gcc 版本节省了 11 个时钟 gcc 没有做 push pop 的事情,所以在 Kiel 上节省了一些时钟,但 nops 没有帮助。
更新:我为 Kiel 设置了错误的循环次数,因为它使用了 unsigned lower 而不是 unsigned lower 或与 gcc 相同。 即使是比赛场地,删除 nops 修复循环 gcc 是 00124F92 和 Kiel 00124F97 由于 push/pop vs sp 数学慢了 5 个时钟。 gcc 5.4.0 也做了 sp 数学运算,nop 00124F93。 在比较这两个(三个)编译器时,在循环之外填充这些差异虽然可测量也在噪音中。
gcc -O1
soft_delay:
sub sp, sp, #8
mov r3, #0
str r3, [sp, #4]
ldr r2, [sp, #4]
ldr r3, .L5
cmp r2, r3
bhi .L1
mov r2, r3
.L3:
ldr r3, [sp, #4]
add r3, r3, #1
str r3, [sp, #4]
ldr r3, [sp, #4]
cmp r3, r2
bls .L3
.L1:
add sp, sp, #8
bx lr
.L6:
.align 2
.L5:
.word 99999
08000200 <soft_delay>:
8000200: b082 sub sp, #8
8000202: 2300 movs r3, #0
8000204: 9301 str r3, [sp, #4]
8000206: 9a01 ldr r2, [sp, #4]
8000208: 4b05 ldr r3, [pc, #20] ; (8000220 <soft_delay+0x20>)
800020a: 429a cmp r2, r3
800020c: d806 bhi.n 800021c <soft_delay+0x1c>
800020e: 1c1a adds r2, r3, #0
8000210: 9b01 ldr r3, [sp, #4]
8000212: 3301 adds r3, #1
8000214: 9301 str r3, [sp, #4]
8000216: 9b01 ldr r3, [sp, #4]
8000218: 4293 cmp r3, r2
800021a: d9f9 bls.n 8000210 <soft_delay+0x10>
800021c: b002 add sp, #8
800021e: 4770 bx lr
8000220: 0001869f muleq r1, pc, r6 ; <UNPREDICTABLE>
000F4251
每个循环 10 个滴答声
8000210: 9b01 ldr r3, [sp, #4]
8000212: 3301 adds r3, #1
8000214: 9301 str r3, [sp, #4]
8000216: 9b01 ldr r3, [sp, #4]
8000218: 4293 cmp r3, r2
800021a: d9f9 bls.n 8000210 <soft_delay+0x10>
与 Kiel 一样,比较值的加载在循环之外,现在每个循环节省一点。 它的架构有点不同。 而且我相信 bls 之后的 nops 是另一回事。 我刚刚看到有人问为什么 gcc 做了一些其他人没有做的事情,这似乎是一个额外的指令。 我会使用术语错过的优化与错误,但无论哪种方式,这个都没有 nops...
gcc -O2 代码
soft_delay:
mov r3, #0
sub sp, sp, #8
str r3, [sp, #4]
ldr r3, [sp, #4]
ldr r2, .L7
cmp r3, r2
bhi .L1
.L3:
ldr r3, [sp, #4]
add r3, r3, #1
str r3, [sp, #4]
ldr r3, [sp, #4]
cmp r3, r2
bls .L3
.L1:
add sp, sp, #8
bx lr
.L8:
.align 2
.L7:
.word 99999
08000200 <soft_delay>:
8000200: 2300 movs r3, #0
8000202: b082 sub sp, #8
8000204: 9301 str r3, [sp, #4]
8000206: 9b01 ldr r3, [sp, #4]
8000208: 4a05 ldr r2, [pc, #20] ; (8000220 <soft_delay+0x20>)
800020a: 4293 cmp r3, r2
800020c: d805 bhi.n 800021a <soft_delay+0x1a>
800020e: 9b01 ldr r3, [sp, #4]
8000210: 3301 adds r3, #1
8000212: 9301 str r3, [sp, #4]
8000214: 9b01 ldr r3, [sp, #4]
8000216: 4293 cmp r3, r2
8000218: d9f9 bls.n 800020e <soft_delay+0xe>
800021a: b002 add sp, #8
800021c: 4770 bx lr
800021e: 46c0 nop ; (mov r8, r8)
8000220: 0001869f
000F4251
与 -O1 没有区别
800020e: 9b01 ldr r3, [sp, #4]
8000210: 3301 adds r3, #1
8000212: 9301 str r3, [sp, #4]
8000214: 9b01 ldr r3, [sp, #4]
8000216: 4293 cmp r3, r2
8000218: d9f9 bls.n 800020e <soft_delay+0xe>
gcc 不愿意从循环中取出第二个负载。
在 -O2 级别,Kiel 为 70005 滴答声和 gcc 1000017。多/慢 42%。
gcc -O3 产生与 -O2 相同的代码。
所以这里的主要区别可能是对 volatile 作用的解释,无论如何,SO 的一些人对它的使用感到不安,但让我们假设这意味着您对变量所做的一切都需要进入/从内存中读取。
从我通常看到的,这意味着
.L3:
ldr r3, [sp, #4]
add r3, r3, #1
str r3, [sp, #4]
ldr r3, [sp, #4]
cmp r3, r2
bls .L3
不是这个
.L3:
ldr r3, [sp, #4]
add r3, r3, #1
str r3, [sp, #4]
cmp r3, r2
bls .L3
这是基尔的bug吗? 你想在这里使用你的过度优化术语吗?
有两个操作一个增量
ldr r3, [sp, #4]
add r3, r3, #1
str r3, [sp, #4]
和一个比较
ldr r3, [sp, #4]
cmp r3, r2
bls .L3
可以说每个人都应该从内存而不是寄存器访问变量。 (在纯粹的调试版本意义上,您也应该看到这样的代码,尽管该工具定义了调试版本的含义)
当您弄清楚您拥有哪个 gcc 以及它是如何使用的时,它可能会解释 gcc 方面的更多代码,即 100% 慢而不是 40%。
我不知道你可以把它做得更紧,我认为重新安排指令也不会提高性能。
此外,这是 gcc 中遗漏的优化:
cmp r3, r2
bhi .L1
gcc 知道它是从零开始的,并且知道它会变成更大的数字,因此 r3 在这里永远不会大于 r2。
我们希望该工具能够做到这一点:
soft_delay:
mov r3, #0
ldr r2, .L7
.L3:
add r3, r3, #1
cmp r3, r2
bls .L3
.L1:
bx lr
.L8:
.align 2
.L7:
.word 99999
00061A88
at 4 instructions per loop on average
但是没有 volatile 它是死代码,因此优化器只会删除它而不是制作此代码。 向下计数循环会稍微小一点
soft_delay:
ldr r2, .L7
.L3:
sub r2, r2, #1
bne .L3
.L1:
bx lr
.L8:
.align 2
.L7:
.word 100000
000493E7
每个循环 3 个滴答声,删除额外的指令有帮助。
Keil过度优化(不太可能,因为代码很简单)
你可能真的在这里,不是因为它很简单,而是 volatile 真正意味着什么,它是否受编译器的解释(我必须找到一个规范)。 这是基尔的错误,是否过度优化?
仍然没有过度优化这样的事情,有一个名字,一个编译器错误。 Kiel 也是这样解释这个错误的,或者 Kiel 和 gcc 在 volatile 的解释上存在分歧。
由于错误的编译器标志,arm-none-eabi-gcc 优化不足(我使用 CLion Embedded plugins` CMakeLists.txt)
出于同样的原因,这也可能是它。 这仅仅是编译器之间的“实现定义”差异,并且两者都基于它们的定义是正确的吗?
现在 gcc 确实错过了这里(或两个)优化,但它占了一小部分,因为它在循环之外。
GCC ||| KEIL
|||
soft_delay: |||
mov r3, #0 |||
sub sp, sp, #8 |||
str r3, [sp, #4] |||
ldr r3, [sp, #4] |||
ldr r2, .L7 |||
cmp r3, r2 |||
bhi .L1 ||| soft_delay PROC
.L3: ||| PUSH {r3,lr}
ldr r3, [sp, #4] ||| MOVS r0,#0
add r3, r3, #1 ||| STR r0,[sp,#0]
str r3, [sp, #4] ||| LDR r0,|L5.20|
ldr r3, [sp, #4] ||| |L5.8|
cmp r3, r2 ||| LDR r1,[sp,#0]
bls .L3 ||| ADDS r1,r1,#1
.L1: ||| STR r1,[sp,#0]
add sp, sp, #8 ||| CMP r1,r0
bx lr ||| BCC |L5.8|
.L7: ||| POP {r3,pc}
.word 1999999 ||| ENDP
KEIL有明显的bug。 volatile
意味着它的值必须在每次使用前加载并在更改时保存。 ? Keil 少了一个负载。
变量使用 2 次:1:增加时,2:比较时。 需要两个负载。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.