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|
11 000006 f64f71ff MOV r1,#0xffff 12 00000a 4288 CMP r0,r1 13 00000c dbfa BLT |L1.4| 14 ;;;12 ;
 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 ;

 

尝试开了一下优化,在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

 

posted @ 2014-03-19 21:37  ppym  阅读(1703)  评论(1编辑  收藏  举报