C/C++ 编程中的内存屏障(Memory Barriers) (1)
明天就要transfor去做檢索引擎了,今天閑下來了,更新一下博客哈。之前 @高V 同學(xué)對(duì)本人之前《代碼技巧及優(yōu)化(c/c++)》的文章第六條,有關(guān)cache命中和cpu流水優(yōu)化比較感興趣,也提出了一些他的看法,今天,我就細(xì)化的說一下某些編程的點(diǎn) -- 內(nèi)存屏障,以及內(nèi)存屏障對(duì)代碼的影響。
OK,首先來說一下什么是"內(nèi)存屏障",可以先看一下官方式的說法http://www.kernel.org/doc/Documentation/memory-barriers.txt,內(nèi)存屏障其實(shí)就是因?yàn)榫幾g器優(yōu)化和CPU對(duì)寄存器和cache的使用,導(dǎo)致對(duì)內(nèi)存的操作不能夠及時(shí)的反映出來,比如cpu寫入后,讀出來的值可能是舊的內(nèi)容。舉個(gè)例子,對(duì)一個(gè)變量賦值然后讀出它的值這一看似“原子”的操作,因?yàn)閮?nèi)存是沒有ALU計(jì)算單元的,所以內(nèi)存沒有計(jì)算的能力。而CPU一般情況下是不直接讀寫內(nèi)存的(emmintrin.h應(yīng)用例外),所以這一個(gè)過程可以看作(編譯器優(yōu)化后):
讀取內(nèi)存數(shù)據(jù)到cache --> CPU讀取cache/寄存器 --> CPU的計(jì)算 --> 將結(jié)果寫入cache/寄存器 --> 寫回?cái)?shù)據(jù)到內(nèi)存
有人可能會(huì)問,為什么要這么麻煩,因?yàn)槭蔷幾g器優(yōu)化和CPU優(yōu)化的結(jié)果,內(nèi)存的時(shí)延比CPU高的多,是10ns級(jí)別,所以會(huì)通過讀寫寄存器或拆cache優(yōu)化。這有可能導(dǎo)致一個(gè)問題,cache中的數(shù)據(jù)和內(nèi)存實(shí)際的數(shù)據(jù)不一致,當(dāng)多線程情況下,有可能會(huì)讀"臟數(shù)據(jù)",或者帶來線程執(zhí)行結(jié)果不一致。這就是所謂的“內(nèi)存屏障”(但不僅限于此case)。
網(wǎng)上有幾篇文章提到的《獨(dú)辟蹊徑品內(nèi)核》中的代碼,本人寫了一個(gè)近似版本如下:
[cpp] 
view plaincopyprint?
include<stdlib.h>#include<stdio.h> #include<pthread.h> 
#include<unistd.h> //globalvariable intflag=1;void*wait(void*context){while(flag){printf("continue\n");sleep(1);}}voidwakeup(){flag=0;}intmain(intargc,char*argv[]){pthread_tpid=0;pthread_create(&pid,NULL,wait,NULL);sleep(3);wakeup();printf("Done\n");getchar();}
按照書中的說法,flag是被其他線程意外修改的話。while循環(huán)會(huì)被編譯器優(yōu)化,編譯器在發(fā)現(xiàn)wait函數(shù)里并沒有修改flag,所以就會(huì)對(duì)flag進(jìn)行cache,放在eax寄存器中,內(nèi)存中的flag實(shí)體如果被修改,eax寄存器不會(huì)感知。自己試了一下,在沒有優(yōu)化和Gcc O2的情況下程序正常跳出了循環(huán),可能是因?yàn)閏pu或者linux新版內(nèi)核或者gcc的新編譯特性所致(如果哪位同學(xué)了解,可以留言哈)。之后會(huì)跟進(jìn)這個(gè)問題,如果有發(fā)現(xiàn),我會(huì)貼出來哈。
其實(shí),通過gcc -S看到匯編過程的中間匯編代碼也可以看出寫東西:
[cpp] 
view plaincopyprint?
wait:.LFB46:.cfi_startprocsubq$8,%rsp.cfi_def_cfa_offset16movlflag(%rip),%edxtestl%edx,%edxje.L4.p2align4,,10.p2align3.L5:movl$.LC0,%edicallputsmovl$1,%edicallsleep<SPANstyle="COLOR:#ff6600">movlflag(%rip),%eaxtestl%eax,%eaxjne.L5</SPAN>.L4:addq$8,%rsp.cfi_def_cfa_offset8ret.cfi_endproc.LFE46:.sizewait,.-wait.p2align4,,15.globlwakeup.typewakeup,@function
(我不是匯編大牛,錯(cuò)了別炮轟哈),其中標(biāo)紅的語句16~18行可以看出,循環(huán)只是檢測(cè)eax寄存器是不是0(不太熟悉匯編的朋友可能會(huì)為什么test %eax,%eax,這只是一個(gè)優(yōu)化因?yàn)榕c操作比cmp要快),可以看出,確實(shí)是在不斷的讀寄存器而不是內(nèi)存。
到此,大家對(duì)“內(nèi)存屏障”估計(jì)也有了一個(gè)初步的認(rèn)識(shí),內(nèi)存屏障主要分三類:編譯器優(yōu)化(如上case) / 緩存優(yōu)化 / CPU亂序執(zhí)行(后面文章會(huì)提到)
大家可能會(huì)問,那豈不這樣會(huì)造成很多問題。其實(shí)不一定,據(jù)本人的知識(shí)范圍,大部分內(nèi)存屏障導(dǎo)致的問題都出現(xiàn)在內(nèi)核態(tài),用戶態(tài)需要注意的方面不多。而且用戶也有相應(yīng)的解決方案,最簡單的就是鎖機(jī)制,還有volatile關(guān)鍵字,可以把可能出現(xiàn)的cache讀臟的數(shù)據(jù)volatile int tmp = 0;這樣每次操作都會(huì)從內(nèi)存獲取。
之后的文章會(huì)深入一下“內(nèi)存屏障”和CPU亂序的問題。緩存優(yōu)化導(dǎo)致的內(nèi)存屏障,基本在新的硬件上得到了比較好的解決,而且在用戶態(tài)下基本感知不到。只要大家合適的用好多線程的鎖機(jī)制和volatile的正確運(yùn)用即可。
總結(jié)
以上是生活随笔為你收集整理的C/C++ 编程中的内存屏障(Memory Barriers) (1)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
                            
                        - 上一篇: redis集群的搭建
 - 下一篇: git 的使用方法