iis服务器助手广告广告
返回顶部
首页 > 资讯 > 后端开发 > 其他教程 >C语言volatile关键字的作用与示例
  • 936
分享到

C语言volatile关键字的作用与示例

C语言volatile关键字C语言volatile 2023-05-15 08:05:37 936人浏览 薄情痞子
摘要

目录写在前面volatile和内联汇编的volatile的选择写在前面 版本信息:linux操作系统,x86架构,Linux操作系统下GCC9.3.1版本。GCC 9.3.0手册。

写在前面

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

看了外面很多写volatile的文章,笔者算是认为“乱七八糟”,根本没有任何论证就在下定义,所以笔者特意写这篇关于volatile的文章。

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

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

笔者有在很多帖子里面看过,他们都一致的说到:volatile可以作为内存屏障保证内存的可见性,这压根就是一个错误的引导,所以这也促使笔者写在这篇文章。

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

// 没优化:
int a = 10;
int b = a;
int c = a;
int d = a;
// 对应的汇编代码
sub 16,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,那么可不可以这样写呢?

// 优化:
int a = 10;
int b = 10;
int c = 10;
int d = 10;
// 对应的汇编代码:
sub 16,esp        // 开辟栈帧
mov $10,(esp-12)  // 把立即数10放入到esp-12的栈帧位置,这也对应a变量。
mov (esp-12),eax  // 把esp-12的栈帧位置对应的值,也就是10放入到eax寄存器中。
mov eax (esp-8)   // 把eax寄存器的值放入到(esp-8)的位置,这也对应b变量
mov eax (esp-4)   // 把eax寄存器的值放入到(esp-4)的位置,这也对应c变量
mov eax (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>

int gnum = 1;

static void pthread_func_1 (void)
{
   while(gnum == 1){
   }
}
int main (void)
{
 
  pthread_t pt_1 = 0;
  int ret = 0;
  
  ret = pthread_create( &pt_1,                  //线程标识符指针
                                     NULL,                  //默认属性
                                     (void *)pthread_func_1,//运行函数
                                     NULL);                  //无参数
  if (ret != 0)
  {
     perror ("pthread_1_create");
  }
  
  sleep(1);
  
  gnum = 0;
  
  pthread_join (pt_1, NULL);
  printf ("main programme exit!/n");
  return 0;
}

这段代码很简单,使用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 -pthread demo.c
// objdump指令查看反汇编
objdump -S a.out
// 反编译后p1线程代码段的汇编代码
000000000040068d <pthread_func_1>:
  40068d: 55                    push   %rbp
  40068e: 48 89 e5              mov    %rsp,%rbp
  400691: 90                    nop
  400692: 8b 05 bc 09 20 00     mov    0x2009bc(%rip),%eax        # 601054 <gnum>       // 每次还从0x2009bc(%rip)获取全局的gnum变量放入eax寄存器
  400698: 83 f8 01              cmp    $0x1,%eax                                        // 拿1和eax寄存器做比较,比较结果放入到flags寄存器中。
  40069b: 74 f5                 je     400692 <pthread_func_1+0x5>                      // 如果比较成功就直接跳到400692这行代码段地址,如果不成功就直接往下执行
  40069d: 5d                    pop    %rbp
  40069e: c3                    retq 

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

gcc -O4编译:

// gcc -O4编译后
gcc -O4 -pthread demo.c
// objdump指令查看反汇编
objdump -S a.out
// 反编译后p1线程代码段的汇编代码
00000000004006f0 <pthread_func_1>:
  4006f0: 83 3D 69 09 20 00 01  cmpl   $0x1,0x200969(%rip)        # 601060 <gnum>       // 比较一次,把结果放入到flags寄存器中
  4006f7: 75 07                 jne    400700 <pthread_func_1+0x10>                     // 如果不等于就直接退出
  4006f9: eb fe                 jmp    4006f9 <pthread_func_1+0x9>                      // 一直循环本行,也就是直接无脑死循环(没有退出条件的死循环)
  4006fb: 0f 1f 44 00 00        nopl   0x0(%rax,%rax,1)
  400700: f3 c3                 repz retq 
  400702: 66 2e 0f 1f 84 00 00  nopw   %cs:0x0(%rax,%rax,1)
  400709: 00 00 00 
  40070c: 0f 1f 40 00           nopl   0x0(%rax)

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

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

volatile修饰后gcc -O4编译:

// volatile修饰后gcc -O4编译: 
gcc -O4 -pthread demo.c
// objdump指令查看反汇编
objdump -S a.out
// 反编译后p1线程代码段的汇编代码
00000000004006f0 <pthread_func_1>:
  4006f0: 8b 05 5e 09 20 00     mov    0x20095e(%rip),%eax        # 601054 <gnum>       // 每次从0x20095e(%rip)获取全局的gnum变量放入eax寄存器
  4006f6: 83 f8 01              cmp    $0x1,%eax                                        // 拿1和eax寄存器做比较,比较结果放入到flags寄存器中。
  4006f9: 74 f5                 je     4006f0 <pthread_func_1>                          // 如果比较成功就直接跳到4006f0这行代码段地址,如果不成功就直接往下执行
  4006fb: f3 c3                 repz retq 
  4006fd: 0f 1f 00              nopl   (%rax)

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

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

int gnum = 1;

static void pthread_func_1 (void)
{
   while(gnum == 1){
     __asm__ __volatile__("": : :"memory")
   }
}
// 使用内联汇编volatile编译器优化: 
gcc -O4 -pthread demo.c
// objdump指令查看反汇编
objdump -S a.out
// 反编译后p1线程代码段的汇编代码
00000000004006f0 <pthread_func_1>:
  4006f0: eb 06                 jmp    4006f8 <pthread_func_1+0x8>
  4006f2: 66 0f 1f 44 00 00     nopw   0x0(%rax,%rax,1)
  4006f8: 83 3d 61 09 20 00 01  cmpl   $0x1,0x200961(%rip)        # 601060 <gnum>       // 拿0x200961(%rip)全局变量gnum的值和1比较。
  4006ff: 74 f7                 je     4006f8 <pthread_func_1+0x8>                      // 如果相等就跳转到4006f8。
  400701: f3 c3                 repz retq 
  400703: 66 2e 0f 1f 84 00 00  nopw   %cs:0x0(%rax,%rax,1)
  40070a: 00 00 00 
  40070d: 0f 1f 00              nopl   (%rax)

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

volatile和内联汇编的volatile的选择

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

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

到此这篇关于C语言volatile关键字的作用与示例的文章就介绍到这了,更多相关C语言volatile关键字内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

--结束END--

本文标题: C语言volatile关键字的作用与示例

本文链接: https://www.lsjlt.com/news/208508.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • C语言volatile关键字的作用与示例
    目录写在前面volatile和内联汇编的volatile的选择写在前面 版本信息:Linux操作系统,x86架构,Linux操作系统下GCC9.3.1版本。GCC 9.3.0手册。 ...
    99+
    2023-05-15
    C语言volatile关键字 C语言volatile
  • C语言中volatile关键字的作用与使用案例教程
    一.前言 1.编译器优化介绍: 由于内存访问速度远不及CPU处理速度,为提高机器整体性能,在硬件上引入硬件高...
    99+
    2022-11-12
  • C语言关键字const与volatile怎么用
    今天小编给大家分享一下C语言关键字const与volatile怎么用的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。一、con...
    99+
    2023-06-30
  • C语言volatile关键字的作用是什么
    本篇内容介绍了“C语言volatile关键字的作用是什么”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!写在前面版本信息:Linux操作系统,...
    99+
    2023-07-06
  • C语言中volatile关键字怎么用
    这篇文章主要介绍了C语言中volatile关键字怎么用,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。一.前言编译器优化介绍:由于内存访问速度远不及CPU处理速度,为提高机器整...
    99+
    2023-06-20
  • C语言中volatile 关键字有什么用
    这期内容当中小编将会给大家带来有关C语言中volatile 关键字有什么用,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。一、关键字 volatile 是什么volatile是一个类型修饰符(type sp...
    99+
    2023-06-16
  • C语言中volatile关键字的深入讲解
    1. 什么是volatile关键字? volatile用于声明一个变量,告诉编译器该变量值容易发生改变,在编译、读取、存储该变量的时候都不要做任何优化,因此编译后的程序每次需要存储...
    99+
    2022-11-12
  • C语言中volatile关键字的详细介绍
    这篇文章主要讲解了“C语言中volatile关键字的详细介绍”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“C语言中volatile关键字的详细介绍”吧!1. 什么是volatile关键字?v...
    99+
    2023-06-20
  • C语言详细分析讲解关键字const与volatile的用法
    目录一、const 只读变量二、const 全局变量的分歧三、const 的本质四、const 修饰函数参数和返回值五、volatile 解析六、小结一、const 只读变量 con...
    99+
    2022-11-13
  • java中volatile关键字的作用与实例代码
    一,什么是volatile关键字,作用是什么  volatile是java虚拟机提供的轻量级同步机制 ​ 作用是: 1.保证可见性 2.禁止指令重排 3.不保...
    99+
    2022-11-12
  • c++ volatile关键字的作用是什么
    在C++中,volatile关键字的作用是告诉编译器不要对变量进行优化,即不要将变量缓存在寄存器中,应该直接从内存中读取或写入变量。...
    99+
    2023-10-28
    c++
  • C++中的volatile关键字及其作用
    volatile是C语言的一个关键字,该关键字的作用是保持内存的可见性 例子: 我们对2号信号进行了捕捉,当该进程收到2号信号时会将全局变量flag由0置1, 也就是说,在进程收到2...
    99+
    2023-05-16
    C++ volatile关键字 C++ volatile关键字的作用
  • Volatile关键字的作用
    Volatile关键字的作用主要有如下两个: 1.线程的可见性:当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。 2. 顺序一致性:禁止指令重排序。 一、线程可见性 我们先通过一个例子来看看线程的可见性: public cla...
    99+
    2023-08-20
    volatile 并发
  • C语言学习之关键字的示例详解
    目录1. 前言2. 什么是关键字3. extern-声明外部符号4. auto-自动5. typedef-类型重定义(类型重命名)6. register-寄存器6.1 存储器6.2 ...
    99+
    2022-11-13
    C语言 关键字
  • 一篇文章带你了解C语言中volatile关键字
    目录C语言中volatile关键字总结C语言中volatile关键字 volatile关键字是C语言中非常冷门的关键字,因为用到这个关键字的场景并不多。 当不用这个关键字的时候,CP...
    99+
    2022-11-12
  • Java中volatile关键字的作用
    目录一、volatile作用二、什么是可见性三、什么是总线锁和缓存锁四、什么是指令重排序一、volatile作用 可以保证多线程环境下共享变量的可见性通过增加内存屏障防止多个指令之间...
    99+
    2022-11-13
  • JAVA并发中VOLATILE关键字的示例分析
    小编给大家分享一下JAVA并发中VOLATILE关键字的示例分析,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!并发编程中的三个概念:1.原子性在Java中,对基本...
    99+
    2023-06-15
  • c语言关键字static的作用是什么
    在C语言中,关键字static有以下几种作用:1. 静态变量:static关键字可以用于声明静态变量,静态变量存储在静态存储区,在程...
    99+
    2023-09-14
    c语言 static
  • c语言register关键字的作用是什么
    C语言中的register关键字用于向编译器建议将变量存储在寄存器中,以便更快地访问。它是一种优化技术,用于提高程序的执行速度。使用...
    99+
    2023-09-16
    c语言
  • C语言typedef关键字有什么作用
    本篇内容主要讲解“C语言typedef关键字有什么作用”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“C语言typedef关键字有什么作用”吧!1、来个笑话赵本山在春晚有一个这样的笑话,是这样的有...
    99+
    2023-06-03
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作