一个程序通常有两个版本
Debug :称为调试版本,它包含调试信息,并且不作任何优化,便于调试和发现问题;
Release :称为发布版本,它按照设定的优化等级进行优化,优化代码空间提高运行速度;
这里不谈论Debug版本,来看看Release版本:以我最熟的keil开发环境为例
此处Optimization(优化等级)正是设置release版本下编译器的优化等级,其默认设置为-O0,这也是为什么我之前在程序中很少使用volatile,但运行不出错的一个原因;下面调整优化等级,看一看区别所在
case1:关闭编译优化,结果如下
case2:优化等级为2级,结果如下
以上对比发现,两种情况的代码量和各种类型数据量均有所不同,这就是编译器优化的结果
补充上述截图中的信息(坑-待填):
Code:
RO-data:
RW-data:
ZI-data:
编译器为什么要优化
很多时候,一些变量的值在运行过程中可能是不变的,若编译器不做优化,每次访问这个变量时都会从内存读出数据,不仅效率低,而且代码量会比较大
编译器优化方式
1:硬件优化
因内存速度限制,CPU访问内存的速度远不及CPU处理速度,但CPU访问寄存器的速度比访问内存的速度要快得多,为提高机器整体性能,在硬件上引入高速缓存Cache,优化对内存的访问,提高效率
2:软件优化
编译器会将它认为不变的量保存在高速缓存cache中,以后CPU需要访问该变量时,就直接在缓存中读取,当数据被更改后,便将其存放在缓存中,但主存中变量的值却未被更新
软件优化带来的一些问题
case1:如下:该程序等待flag标志在中断函数中发生改变,使led闪烁
若编译器进行了优化,将flag变量值存储到cache,但中断服务函数改变的是主存中flag的值,cache中的值并未被改变,为了提高速度,CPU访问的一直是cache中的值,这样程序就达不到我们的目的
int flag=0;
int main()
{
init();
while(flag)
led_switch();
return 0;
}
/* interrupt function */
void IRQn(void)
{
flag = 1;
}
case2:利用for循环语句进行延时,若程序进行优化,编译器会认为for循环没用,删除for循环
case3:一些寄存器的值(如状态寄存器)可能通过硬件来改变主存中的值,若进行优化,则CPU会认为其值一直未变
typedef struct
{
__IO uint32_t MODER; /*!< GPIO port mode register, Address offset: 0x00 */
__IO uint32_t OTYPER; /*!< GPIO port output type register, Address offset: 0x04 */
__IO uint32_t OSPEEDR; /*!< GPIO port output speed register, Address offset: 0x08 */
__IO uint32_t PUPDR; /*!< GPIO port pull-up/pull-down register, Address offset: 0x0C */
__IO uint32_t IDR; /*!< GPIO port input data register, Address offset: 0x10 */
__IO uint32_t ODR; /*!< GPIO port output data register, Address offset: 0x14 */
__IO uint16_t BSRRL; /*!< GPIO port bit set/reset low register, Address offset: 0x18 */
__IO uint16_t BSRRH; /*!< GPIO port bit set/reset high register, Address offset: 0x1A */
__IO uint32_t LCKR; /*!< GPIO port configuration lock register, Address offset: 0x1C */
__IO uint32_t AFR[2]; /*!< GPIO alternate function registers, Address offset: 0x20-0x24 */
} GPIO_TypeDef;
随便打开一个寄存器的定义你会发现,在变量名前面都有__IO修饰,那__IO是什么呢?其实就是volatile关键字
#ifdef __cplusplus
#define __I volatile /*!< Defines 'read only' permissions */
#else
#define __I volatile const /*!< Defines 'read only' permissions */
#endif
#define __O volatile /*!< Defines 'write only' permissions */
#define __IO volatile /*!< Defines 'read / write' permissions */
case4:在一些机器上,会进行如下情况1的配置,若进行了优化,那么编译器会认为变量第一次赋值是无意义的,直接优化为情况2
//情况1
void set_regester(int val)
{
*p_register1 = 1;
*p_register2 = 0;
*p_register2 = val;
*p_register1 = 0;
}
//情况2
void set_regester(int val)
{
*p_register2 = val;
*p_register1 = 0;
}
volatile到底有什么用
编译器对变量进行优化,将变量值存入cache中,如果中断,其他的线程或一些硬件将主存中变量的值改变了,但CPU却不知道,还一味地使用cache中的值。
为了避免这种情况的发生,将变量用volatile关键字进行修饰,让编译器不进行优化,这样一碰到volatile变量,cpu就直接从主存读取数据,这样,该变量就只有一个地方可以访问,数据就不会出现不同步的现象了;
另外volatile会改变编译结果,告诉编译器变量是易变的,不要进行优化,防止出现上面case2和case4的情况
如何观察优化现象
不管编译器怎么优化,最终都会在汇编层面得到体现,如果发现汇编代码始终使用寄存器的副本,那么就可以判定优化级别太高了
声明
以上是个人的一些看法,如有错误,欢迎留言指正