深入理解for循环

1.    

 

         嵌入式开发中,当我们想点亮一个小灯实现闪灭的效果或读写sensor的时候,我们可能需要一个延时函数。

    最简单的延时方法就是用for循环,比如for(int i=0; i < 1000; i++); 但很可能你会因此上当,for循环并没有实现

    预期的延迟。吃过几次亏后,我就记得了要加volatile关键字,为什么这么做后面在分析。下图就是我写的源代码。

    

   

   

 

2 但编译器是怎么优化的

 

    根据前面的结果发现,“编译器优”化和“volatile关键字”是影响for循环能否正常执行的关键。

    但编译器优化和volatile是怎么影响还不清楚。猜想要么减少了指令执行的执行时间,

    或者是加快了运行速度。但还是要看汇编代码才能确定真实的情况。

   

3.汇编代码分析

 3.1

         参考了《深入理解计算机系统》这本书第三章,汇编部分讲的还是比较通俗易懂的。

 linux > gcc -Og  -c test.c -o test.o     编译选项-Og告诉编译器使用会生出符合原始C代码整体结构的机器的优化等级。

 inux > objdump -d  test.o                  反汇编

 gcc -S 就可以产生汇编代码,为什么要反汇编,作者说的很清楚 原因是Gcc产生的汇编代码有点难度,

 而且它包含一些我们不需要的关系的信息。。还是上面的test.c,反汇编后代码如下:

                          图1    (不加volatile 且编译选项为-O0)

-O0选项是不优化, 寄存器组是32位的寄存器,32位指令集有如下寄存器,每种寄存器都有特殊的用途:

(参考《深入理解计算机系统》)

%eax     返回值    ; %ebx 被调用者保存;    %ecx  第四个参数   ;  %edx 第三个参数 ;  

%esi    第二个参数; %edi 第一个参数   ;     %ebp被调用者保存; %esp栈指针;

%r8d   第五个参数;    %r9d第六个参数   ;   %r10d 调用者保存 ; %r11d调用者保存

%r12d 被调用者保存; %r13d 被调用者保存; %r14d 被调用者保存; %r15d 被调用者保存; 

--------------------------------------------------------------------------------------------------

先说下寻找:

第一种类型是立即数(immediate)寻找,立即数的书写方式是‘$’后面跟一个整数,例如movl  $0x4050, %eax ;即0x4050写入eax

第二种类型是寄存器寻找,例如 movw %bp, %sp

第三种是和存储器相关的寻找,例如 movb $17, (%rsp)           间接寻址,出现括号就表示寄存器rsp存储的是地址,17写入rsp所指向的地址

                                                       movq %rax, -12(%rbp)   (基址+偏移量)寻址, 将 rax的值写入(rbp -12)所指向的存储单元

                                                                        

-----------------------------------------------------------------------------------------------------

分析如下:

00000000 <main>:
   0:   55                                    push   %ebp                           压栈,保存ebp,比便恢复ebp的值
   1:   89 e5                               mov    %esp,%ebp                 MOV S,D   S->D       
   3:   83 ec 10                          sub    $0x10,%esp                  SUB S,D  D<- D - S  
   6:   c7 45 fc 00 00 00 00       movl   $0x0,-0x4(%ebp)          mov 0  to   *(ebp -4 )                    int i = 0;
   d:   c7 45 f8 00 00 00 00       movl   $0x0,-0x8(%ebp)          mov 0  to   *(ebp -8)                     int j = 0;
  14:   c7 45 fc 00 00 00 00      movl   $0x0,-0x4(%ebp)          mov 0  to   *(ebp -4 )                    i = 0;
  1b:   eb 1a                              jmp    37 <main+0x37>            jump to37
  1d:   c7 45 f8 00 00 00 00      movl   $0x0,-0x8(%ebp)           mov 0  to  *(ebp - 8)                    j = 0;
  24:   eb 04                              jmp    2a <main+0x2a>            jump  to 2a
  26:   83 45 f8 01                     addl   $0x1,-0x8(%ebp)            *(ebp -8) =  *(ebp -8) + 1             j++;
  2a:   81 7d f8 5f ea 00 00       cmpl   $0xea5f,-0x8(%ebp)      compare 59999 : *(ebp -4)          if (i <= 59999) goto26
  31:   7e f3                               jle    26 <main+0x26>               if <= goto 26
  33:   83 45 fc 01                     addl   $0x1,-0x4(%ebp)             *(ebp -4) = *(ebp -4) + 1              i++;
  37:   81 7d fc 5f ea 00 00       cmpl   $0xea5f,-0x4(%ebp)       compare 59999 : *(ebp -4 )         if ( i <= 59999) goto1d
  3e:   7e dd                              jle    1d <main+0x1d>               If <= goto 1d
  40:   c9                                   leave  
  41:   c3                                    ret 

---------------------------------------------------------------------------------------------------------

mov 与 movl 主要区别在与操作数大小不同,addl也是一样,知道指令的含义即可。

 mov 0  to   *(ebp -4 )  意思是 寄存器ebp存的地址 ,mov指令将0写入 ebp-4所在的内存单元 。

跳转指令有几种不同的编码,最常用的都是PC相对的(PC-relative)。也就是,它们会将目标指令的地址

与紧跟在跳转指令后面那条指令的地址的差作为编码。比如上面蓝色的字体 ,f3 +33 的补码即目标地址26,

dd+40 的补码即目标地址1d。其实知道跳转到哪就行了。第二种 编码方式是给出绝对地址 ,就不赘述了。

----------------------------------------------------------------------------------------------------------   

通过上面的汇编代码我们可以看出,在不优化(-O0)也不加关键字volatile的情况下,程序不断的i++,j++

把结果写到栈上,并且因为用的是间接寻找,所以i和j都存储在存储器上而不是寄存器上。

---------------------------------------------------------------------------------------------------------

 3.2 不加volatile关键,但编译选项带 -O2的方式编译

                       图2 (不加volatile关键字且编译选项为-O2)

可以看到,编译大胆的优化掉了我写的整个程序,直接退出了。

-------------------------------------------------------------------------------------------

 3.3加volatile且编译选项为-O0方式编译

                                      图3 (加volatile关键字 且编译选项为-O0)

从上图可以看出,工作方式和gcc  -O0 -c test.c 基本相同,但有细微的区别:

增加了一个eax寄存器参与运算,从不优化的指令 addl   $0x1,-0x4(%ebp)  

变成了现在的add    $0x1,%eax  mov    %eax,-0x4(%ebp) ; $0x1,%eax  mov    %eax,-0x8(%ebp) 

寄存器的工作处理数据比用存储器快,先用寄存器自加再将结果写入存储器,这便是加volatile关键字后的变化。

------------------------------------------------------------------------------------------------------

 3.4加volatile关键字且编译选项为-O2

                                    图4 (加volatile关键字 且 编译选项为-O2)

从上图可以看出,同样加了关键字volatile后,是否选择优化对程序的执行没有大的影响。

lea    0x0(%esi),%esi      出了延时对程序的执行流程没有影响;这条指令的意思是 esi = esi + 0;

lea    0x0(%edi,%eiz,1),%edi    eiz网上查了下是个寄存器,除了能延时对本程序也没什么影响

65:   89 44 24 08             mov    %eax,0x8(%esp)
69:   8b 44 24 08             mov    0x8(%esp),%eax    这条指令貌似是多余的,因为上一条指令已经使*(esp+8)等于eax,且中间并没有跳转等指令改变eax。

-------------------------------------------------------------------------------------------------------------

小结

          根据上面描述的,整理如下:

         1.相对于编译选项-O0,编译选项-O2会对程序的指令进行优化:可能会移除部分废代码比如看似无用的for循环,

             程序执行的方式也可能改变,比如跳转是选择用jle还是jg,编译器的选择可能会变。

         2.添加关键字volatile对本程序test.c的影响之一是增加了一个寄存器eax工作来加快程序的运算。

            增加关键字volatile也阻止了编译器对相关代码的优化,避免for循环被移除,使程序按正常流程执行。

         3.变量i和j 的值都是保存在存储器中,不是存储在寄存器中;通过增加一个干活的寄存器eax++,程序的程序的运行时间

           从9s多变成了7s,运行的效率有了一点提高。

           

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值