版本信息: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優化后的代碼特別特別的精簡??梢郧宄目吹絤ov 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)地址獲取最新值。所以不會出現無條件的死循環的情況。
在Linux內核中,禁止volatile關鍵字的出現,但是里面都是使用內聯匯編volatile的形式禁止編譯器優化,當然內存屏障也是可以禁止編譯器優化的(對于內存屏障這里點到即可,詳情看不同平臺的操作手冊)。當然Linux內核代碼量特別大,如果很多地方不讓編譯器優化的話,效率會降低,一個操作系統如果性能都不行,那肯定是說不過去的。
如下圖所示:使用了volatile修飾的變量在不同的代碼段之間執行都會影響到代碼段的優化,而內聯匯編volatile就可以按需選擇,就不會全部影響到。所以讀者可以按需選擇。
“C語言volatile關鍵字的作用是什么”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注本站網站,小編將為大家輸出更多高質量的實用文章!
本文由 貴州做網站公司 整理發布,部分圖文來源于互聯網,如有侵權,請聯系我們刪除,謝謝!
c語言中正確的字符常量是用一對單引號將一個字符括起表示合法的字符常量。例如‘a’。數值包括整型、浮點型。整型可用十進制,八進制,十六進制。八進制前面要加0,后面...
2022年天津專場考試原定于3月19日舉行,受疫情影響確定延期,但目前延期后的考試時間推遲。 符合報名條件的考生,須在規定時間登錄招考資訊網(www.zha...
:喜歡聽,樂意看。指很受歡迎?!巴卣官Y料”喜聞樂見:[ xǐ wén lè jiàn ]詳細解釋1. 【解釋】:喜歡聽,樂意看。指很受歡迎。2. 【示例】:這是...
農行企業網銀怎么登錄?企業網銀登錄方法:請用戶登錄農行官網,點擊“企業網銀登錄-證書登錄”進入,原智信版客戶可點擊“用戶名登錄”進入。溫馨提示:若更換登錄電腦需要重新安裝K寶驅動,請用戶通過官網點擊“企業網銀登錄- K寶首次登錄指南安裝K寶驅動”,選擇^下載對應的K寶驅動。辦理企業網銀注冊業務,須向注冊行提供以下資料:1、用戶...
亞馬遜(Amazon,簡稱亞馬遜;納斯達克代碼:AMZN)是美國最大的在線電子商務公司,位于華盛頓州西雅圖市。亞馬遜成立于1995年。起初,它只經營在線圖書銷售,但現在它已經擴展到廣泛的其他產品。成為全球商品種類最多的網絡零售商,全球第二大互聯網企業。亞馬遜和其他供應商為客戶提供數百萬種獨特的新商品、翻新商品和二手商品,如書籍、電影、音樂和游戲、數字下載、電子產品和計算機、家庭園藝用品、玩具、嬰兒...
據新浪科技,近日,網友上傳一則視頻引發熱議,原因是視頻中的一位中國男子長相酷似馬斯克。很多網友稱如果此人不說一口流利的中文,甚至會認為他就是馬斯克。當地時間12月20日上午,馬斯克在推特上回應此視頻稱“我可能是“半個”中國人(Maybe I‘m partly Chinese)”。相關閱讀:三言財經12月20日消息,近日,網友上傳一則視頻引...