for循环产生的Cortex-M3汇编代码的一个奇怪现象
最近比较一下KEIL和IAR两个编译器产生的代码,基于Cortex-M3处理器的,然后发现了一几个奇怪的地方。
很简单的一个C的for循环
1 void fun_for_add_65535(void) 2 { 3 int i; 4 for (i=0; i<65535; i++) 5 ; 6 } 7 8 void fun_for_add_65536(void) 9 { 10 int i; 11 for (i=0; i<65536; i++) 12 ; 13 }
按道理这两个函数除了for的终止值不同之外,产生的汇编代码应该不会有什么差异。但是不是。
先看一下在不优化的情况下产生的 void fun_for_add_65535(void) 汇编代码,IAR不优化是-On选项。
使用fromelf打印出汇编语句如下:
1 1 void fun_for_add_65535(void) 2 2 { 3 3 int i; 4 4 for (i=0; i<65535; i++) 5 \ fun_for_add_65535: 6 \ 00000000 0021 MOVS R1,#+0 7 \ 00000002 0800 MOVS R0,R1 8 \ ??fun_for_add_65535_0: 9 \ 00000004 4FF6FF71 MOVW R1,#+65535 10 \ 00000008 8842 CMP R0,R1 11 \ 0000000A 01DA BGE.N ??fun_for_add_65535_1 12 \ 0000000C 401C ADDS R0,R0,#+1 13 \ 0000000E F9E7 B.N ??fun_for_add_65535_0 14 5 ; 15 6 } 16 \ ??fun_for_add_65535_1: 17 \ 00000010 7047 BX LR ;; return
再来看一下不优化的情况下产生的 void fun_for_add_65536(void) 汇编代码,如下:
首先说明一个是产生的汇编代码中没有进入函数和退出函数时对寄存器的入栈和出栈的封皮,因为没有使用到R4以上的寄存器。
根据ARM 过程调用标准(简称APCS)。
一般来说cortex-m3在编译时会用R0-R3最参数传递,而多余4个的参数需要用到堆栈,返回时返回值放在R0,也就是APCS里约定:每个函数假定R0-R3在函数调用后会改变,而其他的通用寄存器除SP,LR,PC的不会改变,也就是你如果在函数里使用R4,R5等,你要对其进行压栈操作,函数返回时要恢复。因为调用你得函数认为R4,R5它调用完你之后不会改变。
1 1 void fun_for_add_65536(void) 2 2 { 3 3 int i; 4 4 for (i=0; i<65536; i++) 5 \ fun_for_add_65536: 6 \ 00000000 0021 MOVS R1,#+0 7 \ 00000002 0800 MOVS R0,R1 8 \ ??fun_for_add_65536_0: 9 \ 00000004 B0F5803F CMP R0,#+65536 10 \ 00000008 01DA BGE.N ??fun_for_add_65536_1 11 \ 0000000A 401C ADDS R0,R0,#+1 12 \ 0000000C FAE7 B.N ??fun_for_add_65536_0 13 5 ; 14 6 } 15 \ ??fun_for_add_65536_1: 16 \ 0000000E 7047 BX LR ;; return
抛开一些无关信息,直接对比两个汇编文件的关键信息:
fun_for_add_65535: 0021 MOVS R1,#+0 0800 MOVS R0,R1 ??fun_for_add_65535_0: 4FF6FF71 MOVW R1,#+65535 8842 CMP R0,R1 01DA BGE.N ??fun_for_add_65535_1 401C ADDS R0,R0,#+1 F9E7 B.N ??fun_for_add_65535_0 |
fun_for_add_65536: 0021 MOVS R1,#+0 0800 MOVS R0,R1 ??fun_for_add_65536_0: B0F5803F CMP R0,#+65536 01DA BGE.N ??fun_for_add_65536_1 401C ADDS R0,R0,#+1 FAE7 B.N ??fun_for_add_65536_0 |
我把主要的差异点标了出来,很奇怪,为什么65535(0xFFFF)反而要比65536(0x10000)多了一条MOVW的指令,而65536的CMP指令可以直接比较呢?
65535的CMP只有一条THUMB2(8842),应该是因为直接寄存器比较,而65536的则是和立即数比较,所以产生的是B0F5803F 。
总的来说65536的for循环多了两个byte的代码,但是执行时间上快了,因为少了每次比较前的对R1赋值的操作。
IAR产生了这样的代码,看看KEIL产生的代码是怎么样的。
这是 fun_for_add_65535 产生的
1 ;;;7 //char *str = "abcdefg"; 2 ;;;8 void fun_for_add_65535(void) 3 000000 2000 MOVS r0,#0 4 ;;;9 { 5 ;;;10 int i; 6 ;;;11 for (i=0; i<65535; i++) 7 000002 e000 B |L1.6| 8 |L1.4| 9 000004 1c40 ADDS r0,r0,#1 10 |L1.6| 11 000006 f64f71ff MOV r1,#0xffff 12 00000a 4288 CMP r0,r1 13 00000c dbfa BLT |L1.4| 14 ;;;12 ; 15 ;;;13 } 16 00000e 4770 BX lr 17 ;;;14 18 ENDP
这是 fun_for_add_65536产生的,
1 fun_for_add_65536 PROC 2 ;;;15 void fun_for_add_65536(void) 3 000010 2000 MOVS r0,#0 4 ;;;16 { 5 ;;;17 int i; 6 ;;;18 for (i=0; i<65536; i++) 7 000012 e000 B |L1.22| 8 |L1.20| 9 000014 1c40 ADDS r0,r0,#1 10 |L1.22| 11 000016 f5b03f80 CMP r0,#0x10000 12 00001a dbfb BLT |L1.20| 13 ;;;19 ; 14 ;;;20 } 15 00001c 4770 BX lr 16 ;;;21 17 ENDP
同样把主要的差异点标了出来,同样的现象,65536的多了一行MOV赋值给R1的汇编代码
6 ;;;11 for (i=0; i<65535; i++) 7 000002 e000 B |L1.6| 8 |L1.4| 9 000004 1c40 ADDS r0,r0,#1 10 |L1.6| |
6 ;;;18 for (i=0; i<65536; i++) 7 000012 e000 B |L1.22| 8 |L1.20| 9 000014 1c40 ADDS r0,r0,#1 10 |L1.22| |
尝试开了一下优化,在IAR low优化级别下,产生的汇编代码差异还是一样,65535多了一行赋值代码。
5 \ fun_for_add_65535: 6 \ 0020 MOVS R0,#+0 7 \ 00E0 B.N ??fun_for_add_65535_0 8 \ ??fun_for_add_65535_1: 9 \ 401C ADDS R0,R0,#+1 10 \ ??fun_for_add_65535_0: 11 \ 4FF6FF71 MOVW R1,#+65535 12 \ 8842 CMP R0,R1 13 \ FADB BLT.N ??fun_for_add_65535_1 |
5 \ fun_for_add_65536: 6 \ 0020 MOVS R0,#+0 7 \ 00E0 B.N ??fun_for_add_65536_0 8 \ ??fun_for_add_65536_1: 9 \ 401C ADDS R0,R0,#+1 10 \ ??fun_for_add_65536_0: 11 \ B0F5803F CMP R0,#+65536 12 \ FBDB BLT.N ??fun_for_add_65536_1 |
在IAR medium的优化级别下,汇编实现则变了,但总的来说还是65536的实现更加快速一些。
而在KEIL开O1级优化下,两个代码除了初值不同没什么差异。如下所示:
1 ;;;11 for (i=0; i<65535; i++) 2 000002 f64f71ff MOV r1,#0xffff 3 |L1.6| 4 000006 1c40 ADDS r0,r0,#1 5 000008 4288 CMP r0,r1 6 00000a dbfc BLT |L1.6| |
5 ;;;18 for (i=0; i<65536; i++) 6 000010 f44f3180 MOV r1,#0x10000 7 |L1.20| 8 000014 1c40 ADDS r0,r0,#1 9 000016 4288 CMP r0,r1 10 000018 dbfc BLT |L1.20| |
优化之后处理是要好了很多。
甚是奇怪,是编译器的问题吗?继续尝试其他的值,发现大于65535的其他值产生的代码都一样,除了初值不同。然而小于65535的则是截然不同的,在大于4096的循环下产生的汇编代码跟65535的一样,多一条MOVW的指令,小于4096的则和65536的一样,直接CMP比较立即数。然而更加令我惊讶的是,4096的初始值和4097的初始值产生的汇编代码竟然是一样的!如下所示:
4 for (i=0; i<4096; i++) 5 \ fun_for_add_4096: 6 \ 0020 MOVS R0,#+0 7 \ 00E0 B.N ??fun_for_add_4096_0 8 \ ??fun_for_add_4096_1: 9 \ 401C ADDS R0,R0,#+1 10 \ ??fun_for_add_4096_0: 11 \ B0F5805F CMP R0,#+4096 12 \ FBDB BLT.N ??fun_for_add_4096_1 |
21 for (i=0; i<4097; i++) 22 \ fun_for_add_4097: 23 \ 0020 MOVS R0,#+0 24 \ 00E0 B.N ??fun_for_add_4097_0 25 \ ??fun_for_add_4097_1: 26 \ 401C ADDS R0,R0,#+1 27 \ ??fun_for_add_4097_0: 28 \ B0F5805F CMP R0,#+4096 29 \ FBDD BLE.N ??fun_for_add_4097_1 |
这真是让我无比惊讶,在KEIL上面4096和4097也产生了一样的代码,百思不得其解。什么问题?
最后直接在开发板上调试发现循环次数都是正确的。
猛然发现最后一条指令分别是BLT和BLE,查找arm指令手册发现LT是带符号数小于跳转,而LE则是带符号数小于等于跳转,因为4097是多一次比较的。
哈哈,想着不太可能arm编译器出现这样低级的问题的。
那么最后只剩下一个疑问就是上面的多一条MOVW指令的问题。
我的KEIL是4.73版本,IAR是5.3版本。
KEIL的armcc是ARM C/C++ Compiler, 5.03 [Build 76] [MDK-ARM Standard]
IAR的iccarm是IAR ANSI C/C++ Compiler V5.30.2.51295/W32 for ARM