C语言细节 - volatile的重要性

一个程序通常有两个版本

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的情况

如何观察优化现象

不管编译器怎么优化,最终都会在汇编层面得到体现,如果发现汇编代码始终使用寄存器的副本,那么就可以判定优化级别太高了

声明

以上是个人的一些看法,如有错误,欢迎留言指正

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

|清风|

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值