C语言volatie关键字
文章目录
1. 源起
编译器把C源文件编译成目标文件的时候,可能会启用优化。
例如下面代码:
int i = 10;
int main() {
int j = i;
...
int k = i;
}
由于编译器发现两次从i读取数据的代码之间的代码没有对i进行过操作,它会自动把上次读取的数据(一般在寄存器中)放入k中,而不是重新从i里面读取。
然而,编译器对代码的优化并不具备真正的智能,有些情况下变量i的值在int j = i;
和int k = i;
之间改变了但编译器却不能感知到。这样的情况很多,譬如:
- 多线程程序中,另一个线程改变了全局变量i的值
- 嵌入式开发时,在一个中断函数中改变了全局变量i的值
- 嵌入式开发时,i表示一个硬件寄存器,CPU可能通过硬件直接改变i的值
- 。。。
在这些情况下,编译器优化后的目标代码就是不正确的。这个时候就需要volaitle
关键字出场了。
2. volatile关键字
功能:用于修饰变量,它告诉编译器变量的值随时可能发生变化的(即:易变的),每次使用变量的时候必须从变量的地址中重新读取,从而避免了编译器对读取变量的代码执行之前所说的优化。
使用示例:
volatile int i = 10;
int main() {
int j = i;
...
int k = i;
}
一个未使用volatile导致错误的例子
程序源文件如下:
// test.c
// main函数中先后两次读取i的值分别放入a、b,最后比较a和b的值并打印。在两次读取i值之间,通过另一个线程改变i的值。
#include <stdio.h>
#include <pthread.h>
double x = 3;
void wait_long_time() {
for (int64_t n = 0; n < 9999999999; ++n) {
x += 123;
}
}
int i = 10;
void *thread_entry(void *param) {
sleep(3);
printf("thread modify i\n");
i = 6;
return NULL;
}
int main(void){
int a, b;
pthread_t t;
// 创建一个线程t,该线程内延迟3秒后修改i的值
pthread_create(&t, NULL, thread_entry, NULL);
a = i;
wait_long_time(); // 等待一段时间,在该时间内线程t修改了i的值
b = i;
if(a == b){
printf("a = b\n");
}
else {
printf("a != b\n");
}
pthread_join(t, NULL);
return 0;
}
首先使用默认优化级别编译运行:
$ gcc test.c -o test
$ ./test
thread modify i
a != b
程序输出a!=b
,正确。
然后使用-O2
优化级别编译运行:
$ gcc -O2 test.c -o test
$ ./test
thread modify i
a = b
程序输出a=b
,错误。原因自然已经明了。
解决这个问题的方案就是:把int i = 10;
改为volatile int i = 10;
。
修改后再次使用-O2
优化级别编译运行:
$ gcc -O2 test.c -o test
$ ./test
thread modify i
a != b
结果正确,说明volatile
使得编译器对读取变量i
的代码没有进行优化。
3. volatile关键字什么情况下要用
摘自:https://blog.youkuaiyun.com/weixin_38815998/article/details/102840096
3.1 自定义延时函数
#include <stdio.h>
void delay(long val);
int main(){
delay(1000000);
return 0;
}
void delay(long val){
while(val--);
}
相信大佬们对如上程序都挺熟悉的,特别是玩过单片机的同学,主要是通过CPU不断进行无意义的操作达到延时的效果,这种操作如果不启用编译器优化是可以达到预期效果的,但是启用编译器优化就会被优化成如下效果(当然不是在C语言层面上优化,而是在汇编过程优化,只是使用C程序举例):
#include <stdio.h>
void delay(long val);
int main(){
delay(1000000);
return 0;
}
void delay(long val){
;
}
这个时候,delay函数就起不了效果了,需要使用 volatile 修饰 val ;具体可见:
编译器优化对自定义延时程序的影响(volatile详解实验一)
3.2 多线程共享的全局变量
编译器优化对多线程数据同步的影响(volatile详解实验二)
3.3 中断函数与主函数共享的全局变量
中断函数和主函数共享的全局变量需要使用 volatile 修饰的情况是相似的。大家可以感受实验二,去做一个中断的实验。
3.4 硬件寄存器
什么叫硬件寄存器,学过硬件的同学应该不陌生,我们在做按键检测的时候是不是下面这种流程:
1.设置GPIO对应的寄存器配置成输入模式
2.不断地去访问GPIO电平标志寄存器(或者是一个寄存器的标志位)
3.根据寄存器值的某个二进制位确定当前引脚电平。
.
那么有没有想过一个问题,是什么去改变硬件寄存器的值?其实,硬件寄存器上的值的是和底层电路相关的,硬件寄存器的值会影响电路,电路也会反过来影响硬件寄存器的值。
所以在这种情况下,编译器更不应该拷贝副本,而应该每次读写都从内存中读写,保证数据正确,声明成volatile可以防止出现数据出错问题。
附:关于编译器的优化级别
摘自:https://blog.youkuaiyun.com/guanlizhongxintishi/article/details/124644480
- gcc中指定优化级别的参数有:-O0、-O1、-O2、-O3、-Og、-Os、-Ofast。
- 在编译时,如果没有指定上面的任何优化参数,则默认为 -O0,即没有优化。
- 参数 -O1、-O2、-O3 中,随着数字变大,代码的优化程度也越高,不过这在某种意义上来说,也是以牺牲程序的可调试性为代价的。
- 参数 -Og 是在 -O1 的基础上,去掉了那些影响调试的优化,所以如果最终是为了调试程序,可以使用这个参数。不过光有这个参数也是不行的,这个参数只是告诉编译器,编译后的代码不要影响调试,但调试信息的生成还是靠 -g 参数的。
- 参数 -Os 是在 -O2 的基础上,去掉了那些会导致最终可执行程序增大的优化,如果想要更小的可执行程序,可选择这个参数。
- 参数 -Ofast 是在 -O3 的基础上,添加了一些非常规优化,这些优化是通过打破一些国际标准(比如一些数学函数的实现标准)来实现的,所以一般不推荐使用该参数。
- 如果想知道上面的优化参数具体做了哪些优化,可以使用 gcc -Q --help=optimizers 命令来查询,比如下面是查询 -O3 参数开启了哪些优化:
$ gcc -Q --help=optimizers -O3 ... -fassociative-math [disabled] -fassume-phsa [enabled] ...
有关gcc优化的更多详细信息,请参考gcc的官方文档:
https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html#Optimize-Options