前言

本文详解介绍了volatile关键字的原理、使用场景以及一些关键问题。


一、volatile关键字

volatile 关键字在编程中用于告知编译器:该变量的值可能在任何时刻被程序以外的因素(如硬件、中断、其他线程等)修改,强制编译器在每次访问该变量时都直接读写内存(而非依赖寄存器缓存或优化掉相关操作)

二、未使用volatile可能存在的优化场景

1.寄存器缓存优化

编译器发现变量未被显式修改(如循环中反复读取的变量),会将其值缓存在寄存器中,避免重复从内存加载。代码示例:

uint8_t version_flag = 0; /* 在中断中被改写 */
void version_init(void)
{
    while((version_flag != (LOW_FLAG | HIGH_FLAG))) /* 等待初始化完成 */
    {
        ;
    }
}
version_init函数等待version_flag变量被改写后继续运行,version_flag在中断回调函数中被修改。

version_flag变量不加volatile修饰时生成的反汇编代码如下:

void version_init(void)
{
    while((version_flag != (LOW_FLAG | HIGH_FLAG))) 
6003171a:	0003c717          	auipc	a4,0x3c
6003171e:	0ca74703          	lbu	a4,202(a4) # 6006d7e4 <version_flag>
60031722:	478d                li	a5,3
60031724:	00f71063          	bne	a4,a5,60031724 
/* version_flag的值被放在了a4寄存器,值被修改后没有被读取,导致死循环 */
    {
        ;
    }
}

version_flag变量加volatile修饰时生成的反汇编代码如下:

void version_init(void)
{
    while((version_flag != (LOW_FLAG | HIGH_FLAG))) 
6003171a:	0003c697          	auipc	a3,0x3c
6003171e:	0ca68693          	addi	a3,a3,202 # 6006d7e4 <version_flag>
60031722:	470d                li	a4,3
60031724:	0006c783          	lbu	a5,0(a3)
/* version_flag的值被每次都从内存中读取,代码逻辑正确 */
60031728:	fef71ee3          	bne	a4,a5,60031724
    {
        ;
    }
}

2. 死代码消除

若编译器认为变量未被使用或对程序结果无影响,可能直接删除相关代码,代码示例:

void version_init(void)
{
    uint32_t time = 0U;
}

变量没加volatile修饰的反汇编代码:

void version_init(void)
{
    uint32_t time = 0U;
/* 未加volatile修饰,编译器将变量time优化掉 */
}
6003171a:	8082                	ret

变量加了volatile修饰后的反汇编代码:

void version_init(void)
{
6003171a:	1141                	addi	sp,sp,-16
    volatile uint32_t time = 0U;
6003171c:	c602                	sw	zero,12(sp)
/* 加了volatile修饰后编译器会给变量time开辟空间 */
}
6003171e:	0141                	addi	sp,sp,16
60031720:	8082                	ret

3. 循环优化

包括循环不变式外提和循环展开,代码示例:

void version_init(void)
{
    uint32_t i = 0;
    uint32_t value = 0U;
    for (i = 0; i < 10000; i++) {
        value += version_flag;
    }
    printf("version_flag = %d\n", value);
}

version_flag变量不加volatile修饰时生成的反汇编代码如下:

6003171a <version_init>:
6003171a:	6789                lui	a5,0x2
6003171c:	0003c597          	auipc	a1,0x3c
60031720:	0c85c583          	lbu	a1,200(a1) # 6006d7e4 <version_flag>
60031724:	71078793          	addi	a5,a5,1808 # 2710 <__MSP_STACK_LENGTH+0x2610>
60031728:	02f585b3          	mul	a1,a1,a5
/* 编译器将version_flag的值放在了寄存器a1,将value值的计算优化成了乘法指令mul a1,a1,a5,计算错误 */
6003172c:	00013517          	auipc	a0,0x13
60031730:	3b850513          	addi	a0,a0,952 # 60044ae4 <__func__.5417+0x84>
60031734:	86fdd06f          	j	6000efa2 <printf>

version_flag变量加volatile修饰时生成的反汇编代码如下:

void version_init(void)
{
6003171a:	6789                lui	a5,0x2
6003171c:	71078793          	addi	a5,a5,1808 # 2710 <__MSP_STACK_LENGTH+0x2610>
60031720:	4581                li	a1,0
60031722:	0003c697          	auipc	a3,0x3c
60031726:	10268693          	addi	a3,a3,258 # 6006d824 <version_flag>
6003172a:	0006c703          	lbu	a4,0(a3)
/* 代码做了10000次循环,每次都从内存读取version_flag的值做加法,计算正确 */
6003172e:	17fd                addi	a5,a5,-1
60031730:	95ba                add	a1,a1,a4
60031732:	ffe5                bnez	a5,6003172a <version_init+0x10>
60031734:	00013517          	auipc	a0,0x13
60031738:	3c850513          	addi	a0,a0,968 # 60044afc <__func__.5417+0x84>
6003173c:	867dd06f          	j	6000efa2 <printf>

4. 公共子表达式消除

重复读取同一变量的表达式可能被合并为一次读取,代码示例:

uint8_t version_flag = 0; /* 在中断中被改写 */
void version_init(void)
{
    uint32_t value = 0U;
    value += version_flag;
    value += version_flag;

    printf("version_flag = %d\n", value);
}

version_flag变量不加volatile修饰时生成的反汇编代码如下:

void version_init(void)
{
6003171a:	0003c597          	auipc	a1,0x3c
6003171e:	0ca5c583          	lbu	a1,202(a1) # 6006d7e4 <version_flag>
60031722:	0586                slli	a1,a1,0x1
/* 编译器将version_flag的值放在了寄存器a1,将value值的计算优化成了逻辑左移指令slli a1,a1,0x1,如果version_flag中间被中断修改了则计算错误 */
60031724:	00013517          	auipc	a0,0x13
60031728:	3b850513          	addi	a0,a0,952 # 60044adc <__func__.5417+0x84>
6003172c:	877dd06f          	j	6000efa2 <printf>
}

version_flag变量加volatile修饰时生成的反汇编代码如下:

void version_init(void)
{
6003171a:	0003c597          	auipc	a1,0x3c
6003171e:	0ca5c583          	lbu	a1,202(a1) # 6006d7e4 <version_flag>
60031722:	0003c797          	auipc	a5,0x3c
60031726:	0c27c783          	lbu	a5,194(a5) # 6006d7e4 <version_flag>
/* 代码两次从内存读取version_flag的值并分别放在寄存器a1和a5上面,将a1加a5作为最后的计算结果,计算正确 */
6003172a:	95be                add	a1,a1,a5
6003172c:	00013517          	auipc	a0,0x13
60031730:	3c850513          	addi	a0,a0,968 # 60044af4 <__func__.5417+0x84>
60031734:	86fdd06f          	j	6000efa2 <printf>
}

三、什么时候需要使用volatile

1. 硬件寄存器访问

内存映射的硬件寄存器可能会随硬件状态随时变化,而代码中可能没有显式去修改寄存器的值,编译器很有可能认为该寄存器的值未被修改从而不去读取寄存器的内存值,这样就需要加volatile关键字强制访问内存。

2. 中断服务程序(ISR)修改的变量

主循环与 ISR 共享的变量需要加volatile修饰,因为中断服务程序在代码中没有被显式调用,编译器很有可能认为该变量未被修改将其优化掉从而拿不到真正的值。

3. 信号处理程序修改的变量

主程序与信号处理函数共享的变量。

四、volatile修饰的变量是否经过cache?

volatile的作用是强制编译器生成直接访问内存的代码,避免优化导致的逻辑错误;变量是否经过cache由硬件决定,不由volatile关键字决定;如果变量被映射到了cache区域,那么变量就是经过cache的,如果变量被映射到了uncache区域,那么变量就是不经过cache的。

四、volatile修饰指针变量

当想用volatile修饰一个指针变量的时候要将volatile关键字放在 * 号的后面:int * volatile pShort0 ,这样每次访问pShort0变量时会从内存取值;如果放在 * 号前面则表示pShort0指向的变量是必须从内存取值的volatile int * pShort0


总结

编译器的优化目的是为了减少代码访问内存的频率,以此来提高代码的运行速度,因此如果某个变量不直接从内存取值可能会导致代码逻辑错误的话,那么这个变量就需要加volatile关键字修饰以防被编译器优化,最常见的场景还是循环。

Logo

欢迎加入我们的广州开发者社区,与优秀的开发者共同成长!

更多推荐