volatile关键字的作用(C语言volatile关键字的作用是什么)

发布时间:2025-12-10 23:00:00 浏览次数:2

写在前面

版本信息:Linux操作系统,x86架构,Linux操作系统下GCC9.3.1版本。GCC 9.3.0手册。

先看一下GCC文档给的volatile说明:

一言以蔽之:让编译器不再去优化被volatile修饰的变量的操作。但是volatile并不能做内存屏障的功能,想使用内存屏障请使用平台相关的屏障指令,比如GCC提供了一个内联asm volatile ("" : : : "memory");的编译器屏障。详情平台相关的内存屏障请关注特定平台的操作手册~!

既然上述说明了volatile关键字可以避免编译器优化,那么下面笔者用2个列子来说明一下。

//没优化:inta=10;intb=a;intc=a;intd=a;//对应的汇编代码sub16,esp//开辟栈帧mov$10,(esp-12)//把立即数10放入到esp-12的栈帧位置,这也对应a变量。mov(esp-12)(esp-8)//把(esp-12)的值放入到(esp-8)的位置,这也对应b变量mov(esp-12)(esp-4)//把(esp-12)的值放入到(esp-4)的位置,这也对应c变量mov(esp-12)(esp)//把(esp-12)的值放入到(esp)的位置,这也对应d变量//总结,每次从内存中拿

比如这个很简单的列子,定义一个变量a,然后把a赋值给b、c、d。

看汇编代码,可以清楚的看到,每次赋值都是从内存地址中拿去值,这也就需要访问多次内存。影响到代码的执行效率。那么,编译器会如何优化呢?

既然b、c、d都使用的a变量,而A变量为10,那么可不可以这样写呢?

//优化:inta=10;intb=10;intc=10;intd=10;//对应的汇编代码:sub16,esp//开辟栈帧mov$10,(esp-12)//把立即数10放入到esp-12的栈帧位置,这也对应a变量。mov(esp-12),eax//把esp-12的栈帧位置对应的值,也就是10放入到eax寄存器中。moveax(esp-8)//把eax寄存器的值放入到(esp-8)的位置,这也对应b变量moveax(esp-4)//把eax寄存器的值放入到(esp-4)的位置,这也对应c变量moveax(esp)//把eax寄存器的值放入到(esp)的位置,这也对应d变量//总结,每次从eax寄存器拿,此时,可以把eax想成一个缓存寄存器。

可以从汇编代码看出,把a变量的值放入到eax寄存器中,然后把eax寄存器的值赋值给b、c、d变量,这样就只需要访问一次内存了。此时,我们需要考虑,假如赋值b、c、d的过程中,a的值发生了改变了呢?那么对于b、c、d来说还是赋值的原值,所以就出现了问题。

这是一个很简单的编译器优化的例子,代码就是假设的代码,汇编也是伪汇编,那么,为得到读者的认可,笔者也是写了一个真实的案例。

//demo.c案例#include<stdlib.h>#include<stdio.h>#include<pthread.h>#include<errno.h>/*全局变量*/intgnum=1;/*线程1的服务程序*/staticvoidpthread_func_1(void){while(gnum==1){}}intmain(void){/*线程的标识符*/pthread_tpt_1=0;intret=0;/*创建线程1*/ret=pthread_create(&pt_1,//线程标识符指针NULL,//默认属性(void*)pthread_func_1,//运行函数NULL);//无参数if(ret!=0){perror("pthread_1_create");}/*主线程停1秒,让p1线程成功被CPU调度*/sleep(1);/*改变全局属性gnum的值,让p1线程停下来。*/gnum=0;/*等待线程1的结束*/pthread_join(pt_1,NULL);printf("mainprogrammeexit!/n");return0;}

这段代码很简单,使用pthread创建一个p1线程,p1线程里面写了一个while循环,循环条件是判断全局变量gnum是否为1。main线程启动p1线程,同时main线程休眠1秒,让p1线程得到CPU的调度,然后把全局变量gnum设置为0,让p1线程的while结束。main线程使用join等待p1线程执行结束,p1线程结束后main线程打印main programme exit。

gcc普通编译:

//gcc普通编译后gcc-pthreaddemo.c//objdump指令查看反汇编objdump-Sa.out//反编译后p1线程代码段的汇编代码000000000040068d<pthread_func_1>:40068d:55push%rbp40068e:4889e5mov%rsp,%rbp400691:90nop400692:8b05bc092000mov0x2009bc(%rip),%eax#601054<gnum>//每次还从0x2009bc(%rip)获取全局的gnum变量放入eax寄存器400698:83f801cmp$0x1,%eax//拿1和eax寄存器做比较,比较结果放入到flags寄存器中。40069b:74f5je400692<pthread_func_1+0x5>//如果比较成功就直接跳到400692这行代码段地址,如果不成功就直接往下执行40069d:5dpop%rbp40069e:c3retq

可以清楚的看到每次都是从0x2009bc(%rip)获取值给%eax寄存器,然后cmp做比较,je是成功就跳转到400692代码段地址。然后继续mov获取值,cmp比较,je跳转,周而复始......

gcc -O4编译:

//gcc-O4编译后gcc-O4-pthreaddemo.c//objdump指令查看反汇编objdump-Sa.out//反编译后p1线程代码段的汇编代码00000000004006f0<pthread_func_1>:4006f0:833d6909200001cmpl$0x1,0x200969(%rip)#601060<gnum>//比较一次,把结果放入到flags寄存器中4006f7:7507jne400700<pthread_func_1+0x10>//如果不等于就直接退出4006f9:ebfejmp4006f9<pthread_func_1+0x9>//一直循环本行,也就是直接无脑死循环(没有退出条件的死循环)4006fb:0f1f440000nopl0x0(%rax,%rax,1)400700:f3c3repzretq400702:662e0f1f840000nopw%cs:0x0(%rax,%rax,1)400709:00000040070c:0f1f4000nopl0x0(%rax)

这里执行的话就直接死循环了。

这里也比较直观,cmpl比较一次,如果不等于就jne直接返回,如果等于就执行jmp 4006f9,就开始无退出条件的死循环了,不管你后续全局变量gnum值是否改变都无条件死循环。所以这就是编译器优化,导致的问题,那么使用volatile修饰全局变量gnum,看看效果。

volatile修饰后gcc -O4编译:

//volatile修饰后gcc-O4编译:gcc-O4-pthreaddemo.c//objdump指令查看反汇编objdump-Sa.out//反编译后p1线程代码段的汇编代码00000000004006f0<pthread_func_1>:4006f0:8b055e092000mov0x20095e(%rip),%eax#601054<gnum>//每次从0x20095e(%rip)获取全局的gnum变量放入eax寄存器4006f6:83f801cmp$0x1,%eax//拿1和eax寄存器做比较,比较结果放入到flags寄存器中。4006f9:74f5je4006f0<pthread_func_1>//如果比较成功就直接跳到4006f0这行代码段地址,如果不成功就直接往下执行4006fb:f3c3repzretq4006fd:0f1f00nopl(%rax)

volatile 和gcc的O4优化后的代码特别特别的精简。可以清楚的看到mov 0x20095e(%rip),%eax每次都从0x20095e(%rip)地址获取变量给eax寄存器,然后cmp比较,je跳转。所以这跟普通编译的写法是是一样的(单指操作被volatile修饰的变量)

内联汇编volatile修饰后gcc -O4编译:

intgnum=1;/*线程1的服务程序*/staticvoidpthread_func_1(void){while(gnum==1){__asm____volatile__("":::"memory")}}
//使用内联汇编volatile编译器优化:gcc-O4-pthreaddemo.c//objdump指令查看反汇编objdump-Sa.out//反编译后p1线程代码段的汇编代码00000000004006f0<pthread_func_1>:4006f0:eb06jmp4006f8<pthread_func_1+0x8>4006f2:660f1f440000nopw0x0(%rax,%rax,1)4006f8:833d6109200001cmpl$0x1,0x200961(%rip)#601060<gnum>//拿0x200961(%rip)全局变量gnum的值和1比较。4006ff:74f7je4006f8<pthread_func_1+0x8>//如果相等就跳转到4006f8。400701:f3c3repzretq400703:662e0f1f840000nopw%cs:0x0(%rax,%rax,1)40070a:00000040070d:0f1f00nopl(%rax)

这里cmpl直接比较,然后je跳转。比较精简。每次也是从0x200961(%rip)地址获取最新值。所以不会出现无条件的死循环的情况。

volatile和内联汇编的volatile的选择

在Linux内核中,禁止volatile关键字的出现,但是里面都是使用内联汇编volatile的形式禁止编译器优化,当然内存屏障也是可以禁止编译器优化的(对于内存屏障这里点到即可,详情看不同平台的操作手册)。当然Linux内核代码量特别大,如果很多地方不让编译器优化的话,效率会降低,一个操作系统如果性能都不行,那肯定是说不过去的。

如下图所示:使用了volatile修饰的变量在不同的代码段之间执行都会影响到代码段的优化,而内联汇编volatile就可以按需选择,就不会全部影响到。所以读者可以按需选择。

“C语言volatile关键字的作用是什么”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注本站网站,小编将为大家输出更多高质量的实用文章!

volatile关键字的作用
需要做网站?需要网络推广?欢迎咨询客户经理 13272073477