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,运行的效率有了一点提高。