欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

C語(yǔ)言volatile關(guān)鍵字的作用與示例

 更新時(shí)間:2023年04月10日 09:54:42   作者:程序員李哈  
這篇文章主要介紹了C語(yǔ)言volatile關(guān)鍵字的作用,volatile提醒編譯器它后面所定義的變量隨時(shí)都有可能改變,因此編譯后的程序每次需要存儲(chǔ)或讀取這個(gè)變量的時(shí)候,都會(huì)直接從變量地址中讀取數(shù)據(jù)

寫(xiě)在前面

版本信息:Linux操作系統(tǒng),x86架構(gòu),Linux操作系統(tǒng)下GCC9.3.1版本。GCC 9.3.0手冊(cè)。

看了外面很多寫(xiě)volatile的文章,筆者算是認(rèn)為“亂七八糟”,根本沒(méi)有任何論證就在下定義,所以筆者特意寫(xiě)這篇關(guān)于volatile的文章。

先看一下GCC文檔給的volatile說(shuō)明:

一言以蔽之:讓編譯器不再去優(yōu)化被volatile修飾的變量的操作。但是volatile并不能做內(nèi)存屏障的功能,想使用內(nèi)存屏障請(qǐng)使用平臺(tái)相關(guān)的屏障指令,比如GCC提供了一個(gè)內(nèi)聯(lián)asm volatile ("" : : : "memory");的編譯器屏障。詳情平臺(tái)相關(guān)的內(nèi)存屏障請(qǐng)關(guān)注特定平臺(tái)的操作手冊(cè)~!

筆者有在很多帖子里面看過(guò),他們都一致的說(shuō)到:volatile可以作為內(nèi)存屏障保證內(nèi)存的可見(jiàn)性,這壓根就是一個(gè)錯(cuò)誤的引導(dǎo),所以這也促使筆者寫(xiě)在這篇文章。

既然上述說(shuō)明了volatile關(guān)鍵字可以避免編譯器優(yōu)化,那么下面筆者用2個(gè)列子來(lái)說(shuō)明一下。

// 沒(méi)優(yōu)化:
int a = 10;
int b = a;
int c = a;
int d = a;
// 對(duì)應(yīng)的匯編代碼
sub 16,esp             // 開(kāi)辟棧幀
mov $10,(esp-12)       // 把立即數(shù)10放入到esp-12的棧幀位置,這也對(duì)應(yīng)a變量。
mov (esp-12) (esp-8)   // 把(esp-12)的值放入到(esp-8)的位置,這也對(duì)應(yīng)b變量
mov (esp-12) (esp-4)   // 把(esp-12)的值放入到(esp-4)的位置,這也對(duì)應(yīng)c變量
mov (esp-12) (esp)     // 把(esp-12)的值放入到(esp)的位置,這也對(duì)應(yīng)d變量
// 總結(jié),每次從內(nèi)存中拿

比如這個(gè)很簡(jiǎn)單的列子,定義一個(gè)變量a,然后把a(bǔ)賦值給b、c、d。

看匯編代碼,可以清楚的看到,每次賦值都是從內(nèi)存地址中拿去值,這也就需要訪問(wèn)多次內(nèi)存。影響到代碼的執(zhí)行效率。那么,編譯器會(huì)如何優(yōu)化呢?

既然b、c、d都使用的a變量,而A變量為10,那么可不可以這樣寫(xiě)呢?

// 優(yōu)化:
int a = 10;
int b = 10;
int c = 10;
int d = 10;
// 對(duì)應(yīng)的匯編代碼:
sub 16,esp        // 開(kāi)辟棧幀
mov $10,(esp-12)  // 把立即數(shù)10放入到esp-12的棧幀位置,這也對(duì)應(yīng)a變量。
mov (esp-12),eax  // 把esp-12的棧幀位置對(duì)應(yīng)的值,也就是10放入到eax寄存器中。
mov eax (esp-8)   // 把eax寄存器的值放入到(esp-8)的位置,這也對(duì)應(yīng)b變量
mov eax (esp-4)   // 把eax寄存器的值放入到(esp-4)的位置,這也對(duì)應(yīng)c變量
mov eax (esp)     // 把eax寄存器的值放入到(esp)的位置,這也對(duì)應(yīng)d變量
// 總結(jié),每次從eax寄存器拿,此時(shí),可以把eax想成一個(gè)緩存寄存器。

可以從匯編代碼看出,把a(bǔ)變量的值放入到eax寄存器中,然后把eax寄存器的值賦值給b、c、d變量,這樣就只需要訪問(wèn)一次內(nèi)存了。此時(shí),我們需要考慮,假如賦值b、c、d的過(guò)程中,a的值發(fā)生了改變了呢?那么對(duì)于b、c、d來(lái)說(shuō)還是賦值的原值,所以就出現(xiàn)了問(wèn)題。

這是一個(gè)很簡(jiǎn)單的編譯器優(yōu)化的例子,代碼就是假設(shè)的代碼,匯編也是偽匯編,那么,為得到讀者的認(rèn)可,筆者也是寫(xiě)了一個(gè)真實(shí)的案例。

// demo.c案例
#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>
#include <errno.h>
/*全局變量*/
int gnum = 1;
/*線程1的服務(wù)程序*/
static void pthread_func_1 (void)
{
   while(gnum == 1){
   }
}
int main (void)
{
 /*線程的標(biāo)識(shí)符*/
  pthread_t pt_1 = 0;
  int ret = 0;
  /*
    創(chuàng)建線程1
  */
  ret = pthread_create( &pt_1,                  //線程標(biāo)識(shí)符指針
                                     NULL,                  //默認(rèn)屬性
                                     (void *)pthread_func_1,//運(yùn)行函數(shù)
                                     NULL);                  //無(wú)參數(shù)
  if (ret != 0)
  {
     perror ("pthread_1_create");
  }
  /* 主線程停1秒,讓p1線程成功被CPU調(diào)度 */
  sleep(1);
  /* 改變?nèi)謱傩詆num的值,讓p1線程停下來(lái)。 */
  gnum = 0;
  /* 等待線程1的結(jié)束 */
  pthread_join (pt_1, NULL);
  printf ("main programme exit!/n");
  return 0;
}

這段代碼很簡(jiǎn)單,使用pthread創(chuàng)建一個(gè)p1線程,p1線程里面寫(xiě)了一個(gè)while循環(huán),循環(huán)條件是判斷全局變量gnum是否為1。main線程啟動(dòng)p1線程,同時(shí)main線程休眠1秒,讓p1線程得到CPU的調(diào)度,然后把全局變量gnum設(shè)置為0,讓p1線程的while結(jié)束。main線程使用join等待p1線程執(zhí)行結(jié)束,p1線程結(jié)束后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寄存器做比較,比較結(jié)果放入到flags寄存器中。
  40069b: 74 f5                 je     400692 <pthread_func_1+0x5>                      // 如果比較成功就直接跳到400692這行代碼段地址,如果不成功就直接往下執(zhí)行
  40069d: 5d                    pop    %rbp
  40069e: c3                    retq 

可以清楚的看到每次都是從0x2009bc(%rip)獲取值給%eax寄存器,然后cmp做比較,je是成功就跳轉(zhuǎn)到400692代碼段地址。然后繼續(xù)mov獲取值,cmp比較,je跳轉(zhuǎn),周而復(fù)始......

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>       // 比較一次,把結(jié)果放入到flags寄存器中
  4006f7: 75 07                 jne    400700 <pthread_func_1+0x10>                     // 如果不等于就直接退出
  4006f9: eb fe                 jmp    4006f9 <pthread_func_1+0x9>                      // 一直循環(huán)本行,也就是直接無(wú)腦死循環(huán)(沒(méi)有退出條件的死循環(huán))
  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)

這里執(zhí)行的話(huà)就直接死循環(huán)了。

這里也比較直觀,cmpl比較一次,如果不等于就jne直接返回,如果等于就執(zhí)行jmp 4006f9,就開(kāi)始無(wú)退出條件的死循環(huán)了,不管你后續(xù)全局變量gnum值是否改變都無(wú)條件死循環(huán)。所以這就是編譯器優(yōu)化,導(dǎo)致的問(wèn)題,那么使用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寄存器做比較,比較結(jié)果放入到flags寄存器中。
  4006f9: 74 f5                 je     4006f0 <pthread_func_1>                          // 如果比較成功就直接跳到4006f0這行代碼段地址,如果不成功就直接往下執(zhí)行
  4006fb: f3 c3                 repz retq 
  4006fd: 0f 1f 00              nopl   (%rax)

volatile 和gcc的O4優(yōu)化后的代碼特別特別的精簡(jiǎn)??梢郧宄目吹絤ov 0x20095e(%rip),%eax每次都從0x20095e(%rip)地址獲取變量給eax寄存器,然后cmp比較,je跳轉(zhuǎn)。所以這跟普通編譯的寫(xiě)法是是一樣的(單指操作被volatile修飾的變量)

內(nèi)聯(lián)匯編volatile修飾后gcc -O4編譯:

int gnum = 1;
/*線程1的服務(wù)程序*/
static void pthread_func_1 (void)
{
   while(gnum == 1){
     __asm__ __volatile__("": : :"memory")
   }
}
// 使用內(nèi)聯(lián)匯編volatile編譯器優(yōu)化: 
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>                      // 如果相等就跳轉(zhuǎn)到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跳轉(zhuǎn)。比較精簡(jiǎn)。每次也是從0x200961(%rip)地址獲取最新值。所以不會(huì)出現(xiàn)無(wú)條件的死循環(huán)的情況。

volatile和內(nèi)聯(lián)匯編的volatile的選擇

在Linux內(nèi)核中,禁止volatile關(guān)鍵字的出現(xiàn),但是里面都是使用內(nèi)聯(lián)匯編volatile的形式禁止編譯器優(yōu)化,當(dāng)然內(nèi)存屏障也是可以禁止編譯器優(yōu)化的(對(duì)于內(nèi)存屏障這里點(diǎn)到即可,詳情看不同平臺(tái)的操作手冊(cè))。當(dāng)然Linux內(nèi)核代碼量特別大,如果很多地方不讓編譯器優(yōu)化的話(huà),效率會(huì)降低,一個(gè)操作系統(tǒng)如果性能都不行,那肯定是說(shuō)不過(guò)去的。

如下圖所示:使用了volatile修飾的變量在不同的代碼段之間執(zhí)行都會(huì)影響到代碼段的優(yōu)化,而內(nèi)聯(lián)匯編volatile就可以按需選擇,就不會(huì)全部影響到。所以讀者可以按需選擇。

到此這篇關(guān)于C語(yǔ)言volatile關(guān)鍵字的作用與示例的文章就介紹到這了,更多相關(guān)C語(yǔ)言volatile關(guān)鍵字內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • VisualStudio2019解決scanf函數(shù)報(bào)錯(cuò)問(wèn)題

    VisualStudio2019解決scanf函數(shù)報(bào)錯(cuò)問(wèn)題

    在 Visual Studio 2019 編輯代碼時(shí),前期剛剛接觸到VS編譯器時(shí)存在的困惑,當(dāng)用scanf()函數(shù),進(jìn)行輸入時(shí),在運(yùn)行的時(shí)候編譯器會(huì)出現(xiàn)警告報(bào)錯(cuò),本文就來(lái)介紹一下解決方法
    2023-08-08
  • C++內(nèi)存管理面經(jīng)

    C++內(nèi)存管理面經(jīng)

    這篇文章主要介紹了C++的內(nèi)存分配方式以及介紹了下棧和堆的區(qū)別,感興趣的小伙伴可以參考閱讀本文
    2023-03-03
  • C++中抽象類(lèi)和接口的區(qū)別介紹

    C++中抽象類(lèi)和接口的區(qū)別介紹

    抽象類(lèi)(abstract class)和接口(interface)的概念是面向?qū)ο笤O(shè)計(jì)中常用的概念, 也是比較容易混淆的概念. 在這里, 我提出一種區(qū)分它們的思路
    2013-04-04
  • C 語(yǔ)言基礎(chǔ)之C 語(yǔ)言三大語(yǔ)句注意事項(xiàng)

    C 語(yǔ)言基礎(chǔ)之C 語(yǔ)言三大語(yǔ)句注意事項(xiàng)

    今天講解的內(nèi)容,則是自己對(duì)于這三種語(yǔ)句一些細(xì)節(jié)的簡(jiǎn)單介紹,分支語(yǔ)句:if,switch、循環(huán)語(yǔ)句:while,for,do while、goto語(yǔ)句,感興趣的小伙伴可以參考下面具體的文章內(nèi)容
    2021-09-09
  • C++實(shí)現(xiàn)一個(gè)簡(jiǎn)單的SOAP客戶(hù)端

    C++實(shí)現(xiàn)一個(gè)簡(jiǎn)單的SOAP客戶(hù)端

    這篇文章主要介紹了C++實(shí)現(xiàn)一個(gè)簡(jiǎn)單的SOAP客戶(hù)端,在C++中,一般使用gSOAP來(lái)實(shí)現(xiàn)客戶(hù)端、服務(wù)端,下面一起進(jìn)入文章了解具體內(nèi)容,需要的朋友可以參考一下
    2021-11-11
  • C++不使用變量求字符串長(zhǎng)度strlen函數(shù)的實(shí)現(xiàn)方法

    C++不使用變量求字符串長(zhǎng)度strlen函數(shù)的實(shí)現(xiàn)方法

    這篇文章主要介紹了C++不使用變量求字符串長(zhǎng)度strlen函數(shù)的實(shí)現(xiàn)方法,實(shí)例分析了strlen函數(shù)的實(shí)現(xiàn)原理與不使用變量求字符串長(zhǎng)度的實(shí)現(xiàn)技巧,需要的朋友可以參考下
    2015-06-06
  • C語(yǔ)言:十進(jìn)制,BCD碼互換詳解

    C語(yǔ)言:十進(jìn)制,BCD碼互換詳解

    這篇文章主要介紹了C語(yǔ)言十進(jìn)制,BCD碼互換實(shí)例,小編覺(jué)得這篇文章寫(xiě)的還不錯(cuò),實(shí)例簡(jiǎn)單明了,需要的朋友可以參考下
    2021-09-09
  • 淺析如何在c語(yǔ)言中調(diào)用Linux腳本

    淺析如何在c語(yǔ)言中調(diào)用Linux腳本

    如何在c語(yǔ)言中調(diào)用Linux腳本呢?下面小編就為大家詳細(xì)的介紹一下吧!需要的朋友可以過(guò)來(lái)參考下
    2013-08-08
  • C語(yǔ)言使用rand函數(shù)生成隨機(jī)數(shù)

    C語(yǔ)言使用rand函數(shù)生成隨機(jī)數(shù)

    這篇文章介紹了C語(yǔ)言使用rand函數(shù)生成隨機(jī)數(shù)的方法,對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2021-12-12
  • C++?路徑中./、../、/代表的含義

    C++?路徑中./、../、/代表的含義

    這篇文章主要介紹了C++?路徑中./、../、/代表的含義,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2021-12-12

最新評(píng)論