最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會員登陸 & 注冊

并發(fā)編程:亂序執(zhí)行的那些事兒

2023-07-15 14:37 作者:小林家的垃圾王R  | 我要投稿


并發(fā)編程:亂序執(zhí)行的那些事兒

FOCUS

什么是亂序執(zhí)行

亂序執(zhí)行,簡單說就是程序里面的代碼的執(zhí)行順序,有可能會被編譯器、CPU 根據(jù)某種策略調(diào)整順序(俗稱,“打亂”)——雖然從單線程的角度看,亂序執(zhí)行不影響執(zhí)行結(jié)果。

為什么需要亂序執(zhí)行

主要原因是 CPU 內(nèi)部采用流水線技術(shù)。抽象且簡化地看,一個 CPU 指令的執(zhí)行過程可以分成 4 個階段:取指、譯碼、執(zhí)行、寫回。

這 4 個階段分別由 4 個獨立物理執(zhí)行單元來完成。這種情況下,如果指令之間沒有依賴關(guān)系,后一條指令并不需要等到前一條指令完全執(zhí)行完成再開始執(zhí)行。而是前一條指令完成取指之后,后一條指令便可以開始執(zhí)行取指操作。

比較理想的情況如下圖所示:指令之間無依賴,可以使流水線的并行度最大化。


按序執(zhí)行的情況下,一旦遇到指令依賴的情況,流水線就會停滯。比如:

指令 1: Load R3 <- R1(0) ? ?# 從內(nèi)存中加載數(shù)據(jù)到 R3 寄存器 指令 2: Add ?R3 <- R3, R1 ? # 加法,依賴指令 1 的執(zhí)行結(jié)果 指令 3: Sub ?R1 <- R6, R7 ? # 減法 指令 4: Add ?R4 <- R6, R8 ? # 加法

上面的偽代碼中,指令 2 依賴指令 1 的執(zhí)行結(jié)果。在指令 1 執(zhí)行完成之前,指令 2 無法執(zhí)行,這會讓流水線的執(zhí)行效率大大降低。


觀察到,指令 3 和指令 4 對其它指令沒有依賴,可以考慮將這兩條指令”亂序“到指令 2 之前。

這樣,流水線執(zhí)行單元就可以盡可能處于工作狀態(tài)。

總的來說,通過亂序執(zhí)行,合理調(diào)整指令的執(zhí)行順序,可以提高流水線的運行效率,讓指令的執(zhí)行能夠盡可能地并行起來。

Compiler Fence

在多線程的環(huán)境下,亂序執(zhí)行的存在,很容易打破一些預(yù)期,造成一些意想不到的問題。

亂序執(zhí)行有兩種情況:

  1. 在編譯期,編譯器進(jìn)行指令重排。

  2. 在運行期,CPU 進(jìn)行指令亂序執(zhí)行。

我們先來看一個編譯器指令重排的例子:

#include <atomic> // 按序遞增發(fā)號器 std::atomic<int> timestamp_oracle{0}; // 當(dāng)前處理的號碼 int now_serving_ts{0}; int shared_value; int compute(); void memory_reorder() { ? ?// 原子地獲取一個號碼 ? ?int ts = timestamp_oracle.fetch_add(1); ? ?// 加鎖:判斷當(dāng)前是否輪到這個號碼,否則就循環(huán)等 ? ?while (now_serving_ts != ts); ? ?// 臨界區(qū):開始處理請求 ? ?shared_value = compute(); ? ? ? ?// 編譯器 memory barrier ? ?asm volatile("" : : : "memory"); ? ?// 解鎖:下一個要處理的號碼 ? ?now_serving_ts = ts + 1; }

簡單解釋一下這段代碼:

  1. 這個程序通過維護(hù)一個“發(fā)號器 timestamp_oracle”,來實現(xiàn)按順序處理每個線程的請求。

  2. 每個線程先從“發(fā)號器”取一個號,然后不停判斷當(dāng)前是否輪到自己執(zhí)行,類似自旋鎖的邏輯。

  3. 每個線程執(zhí)行完,將“號碼”切換到下一個。

在 O1 的編譯優(yōu)化選項下,編譯出來的匯編指令沒有被重排(通過左右兩邊的代碼行背景色就可以看出來)。


在 O2 的編譯優(yōu)化選項下,出現(xiàn)了指令被重排了,并且這里的指令重排打破了程序的預(yù)期,先切換了 now_serving_ts,再更新 shared_value,導(dǎo)致 shared_value 可能被并發(fā)修改。


為了阻止編譯器重排這兩句代碼的指令,需要在它們之間插入一個 compiler fence。


asm volatile("": : :"memory");

這個是 GCC 擴(kuò)展的 compiler fence 的寫法。這條指令告訴編譯器(GCC 官方文檔):

  1. 防止這條 fence 指令上方的內(nèi)存訪問操作被移到下方,同時防止下方的內(nèi)存訪問操作移到上面,也就是防止了亂序。

  2. 讓編譯器把所有緩存在寄存器中的內(nèi)存變量 flush 到內(nèi)存中,然后重新從內(nèi)存中讀取這些值。

對于第 2 點,有時候我們只需要刷新部分變量。刷新所有寄存器并不一定完全符合我們的預(yù)期,并且會引入不必要的開銷。GCC 支持指定變量的 compiler fence。

write(x) asm volatile("": "=m"(y) : "m"(x):) read(y)

中間的內(nèi)聯(lián)匯編指令告訴編譯器不要把 write(x) 和 read(y) 這兩個操作亂序。

CPU Fence

先來看一個例子:

int x = 0; int y = 0; int r0, r1; // CPU1 void f1() { ? ?x = 1; ? ?asm volatile("": : :"memory"); ? ?r0 = y; } // CPU2 void f2() { ? ?y = 1; ? ?asm volatile("": : :"memory"); ? ?r1 = x; }

上面的例子中,由于 compiler fence 的存在,編譯器不會對函數(shù) f1 和函數(shù) f2 內(nèi)部的指令進(jìn)行重排。

此時,如果 CPU 執(zhí)行時也沒有亂序,是不可能出現(xiàn) r0 == 0 && r1 == 0 的情況的。不幸的是,由于 CPU 亂序執(zhí)行的存在,這種情況是可能發(fā)生的??聪旅孢@個例子:

#include <iostream> #include <thread> int x = 0; int y = 0; int r0 = 100; int r1 = 100; void f1() { ? ?x = 1; ? ?asm volatile("": : :"memory"); ? ?r0 = y; } void f2() { ? ?y = 1; ? ?asm volatile("": : :"memory"); ? ?r1 = x; } void init() { ? ?x = 0; ? ?y = 0; ? ?r0 = 100; ? ?r1 = 100; } bool check() { ? ?return r0 == 0 && r1 == 0; } std::atomic<bool> wait1{true}; std::atomic<bool> wait2{true}; std::atomic<bool> stop{false}; void loop1() { ? ?while(!stop.load(std::memory_order_relaxed)) { ? ? ? ?while (wait1.load(std::memory_order_relaxed)); ? ? ? ?asm volatile("" ::: "memory"); ? ? ? ?f1(); ? ? ? ?asm volatile("" ::: "memory"); ? ? ? ?wait1.store(true, std::memory_order_relaxed); ? ?} } void loop2() { ? ?while (!stop.load(std::memory_order_relaxed)) { ? ? ? ?while (wait2.load(std::memory_order_relaxed)); ? ? ? ?asm volatile("" ::: "memory"); ? ? ? ?f2(); ? ? ? ?asm volatile("" ::: "memory"); ? ? ? ?wait2.store(true, std::memory_order_relaxed); ? ?} } int main() { ? ?std::thread thread1(loop1); ? ?std::thread thread2(loop2); ? ?long count = 0; ? ?while(true) { ? ? ? ?count++; ? ? ? ?init(); ? ? ? ?asm volatile("" ::: "memory"); ? ? ? ?wait1.store(false, std::memory_order_relaxed); ? ? ? ?wait2.store(false, std::memory_order_relaxed); ? ? ? ?while (!wait1.load(std::memory_order_relaxed)); ? ? ? ?while (!wait2.load(std::memory_order_relaxed)); ? ? ? ?asm volatile("" ::: "memory"); ? ? ? ?if (check()) { ? ? ? ? ? ?std::cout << "test count " << count << ": r0 == " << r0 << " && r1 == " << r1 << std::endl; ? ? ? ? ? ?break; ? ? ? ?} else { ? ? ? ? ? ?if (count % 10000 == 0) { ? ? ? ? ? ? ? ?std::cout << "test count " << count << ": OK" << std::endl; ? ? ? ? ? ?} ? ? ? ?} ? ?} ? ?stop.store(true); ? ?wait1.store(false); ? ?wait2.store(false); ? ?thread1.join(); ? ?thread2.join(); ? ?return 0; }

上面的程序可以很輕易就運行出 r0 == 0 && r1 == 0 的結(jié)果,比如:

test count 56: r0 == 0 && r1 == 0

為了防止 CPU 亂序執(zhí)行,需要使用 CPU fence。我們可以將函數(shù) f1 和 f2 中的 compiler fence 修改為 CPU fence:

void f1() { ? ?x = 1; ? ?asm volatile("mfence": : :"memory"); ? ?r0 = y; } void f2() { ? ?y = 1; ? ?asm volatile("mfence": : :"memory"); ? ?r1 = x; }

如此,便不會出現(xiàn) r0 == 0 && r1 == 0 的情況了。

總結(jié)

指令亂序執(zhí)行主要由兩種因素導(dǎo)致:

  1. 編譯期指令重排。

  2. 運行期 CPU 亂序執(zhí)行。

無論是編譯期的指令重排還是 CPU 的亂序執(zhí)行,主要都是為了讓 CPU 內(nèi)部的指令流水線可以“充滿”,提高指令執(zhí)行的并行度。

上面舉的插入 fence 的例子都是使用了 GCC 的擴(kuò)展語法,實際上 C++ 標(biāo)準(zhǔn)庫已經(jīng)提供了類似的封裝:std::atomic_thread_fence,跨平臺且可讀性更好。

一些無鎖編程、追求極致性能的場景可能會需要手動在合適的地方插入合適 fence,這里涉及的細(xì)節(jié)太多,非常容易出錯。原子變量操作根據(jù)不同的 memory order 會自動插入合適的 fence,建議優(yōu)先考慮使用原子變量。

發(fā)布于 2021-09-25 16:59

并發(fā)

評論千萬條,友善第一條


6 條評論

默認(rèn)

最新

不吃香菜的大頭怪

x86會亂序執(zhí)行但是結(jié)果會順序提交 造成x86 TSO的原因是store buffer

2021-12-15

追萊恩

是的,x86只有示例這種StoreLoad亂序

05-06

CopyCoder

越學(xué)越不會了。

2022-07-19

天高地遠(yuǎn)

老哥,這個左邊c++右邊匯編的軟件或者網(wǎng)站是什么呀,感覺好方便

05-31

天高地遠(yuǎn)

FOCUS

非常感謝

05-31

FOCUS

作者

godbolt.org/

05-31



Intel CPU漏洞技術(shù)解讀:都是緩存惹的禍!

云杉網(wǎng)絡(luò)

技術(shù)創(chuàng)造價值


背景

2017年6月1日,Google的安全團(tuán)隊向Intel、AMD、ARM報了一個硬件級的漏洞,造成的危害是內(nèi)核數(shù)據(jù)泄露,修復(fù)該漏洞的代價是至少30%的性能損失。2017年末Linux內(nèi)核社區(qū)推出了KPTI「Kernel Page Table Isolation」補丁,Linus Torvalds在內(nèi)核郵件列表上毫不留情地抨擊了Intel。

安全人員將這兩個漏洞命名為MeltdownSpectre;Meltdown目前只存在于Intel的處理器和部分ARM處理器,Spectre存在于一切有亂序執(zhí)行的現(xiàn)代處理器架構(gòu)里面,包括AMD。從原理上來說漏洞無法徹底修復(fù)。

本次的漏洞會對所有云廠商造成較大影響,已經(jīng)有跡象表明有黑客在利用漏洞攻擊云系統(tǒng)。Microsoft Azure中國區(qū)已發(fā)布公告稱,將于北京時間2018 年 1 月 4 日上午 11:30 開始自動重啟受影響的虛擬機(jī),并全部關(guān)閉向部分客戶開放的自助維護(hù)窗口;AWS也發(fā)送了通知郵件聲稱本周五將進(jìn)行重大安全更新。

原因

一切還是要從CPU指令執(zhí)行的框架——流水線說起。Intel當(dāng)然不至于明知你要用一個用戶態(tài)的進(jìn)程讀取Kernel內(nèi)存還會給你許可。但現(xiàn)代CPU流水線的設(shè)計,尤其是和性能優(yōu)化相關(guān)的流水線的特性,讓這一切充滿了變數(shù)。

給所有還沒有看過云杉網(wǎng)絡(luò)連載的系列文章《x86高性能編程箋注系列》的讀者一點背景知識的介紹:

x86 CPU為了優(yōu)化性能,在處理器架構(gòu)方面做了很多努力。諸如“多級緩存”這一類的特性,是大家都比較熟悉的概念。還有一些特性,比如分支預(yù)測和亂序執(zhí)行,也都是一些可以從并行性等方面有效提升程序性能的特性,并且它們也都是組成流水線的幾個關(guān)鍵環(huán)節(jié)。即便你暫時還不能準(zhǔn)確理解其含義,但望文生義,也能看出來這肯定是兩個熵增的過程。熵增帶來無序,無序就會帶來更多漏洞。

緩存的困境

講緩存,必然先掛一張memory hierarchy鎮(zhèn)樓:

不過我要說的和這個沒太大關(guān)系?,F(xiàn)在需要考慮的是,如果能讀取到內(nèi)核地址的內(nèi)容,那這部分內(nèi)容最終肯定是跑到緩存中去了,因為真正直接和CPU核心交互的存儲器,就是緩存。這對一級緩存(L1 Cache,業(yè)內(nèi)也常用縮寫L1$,取cash之音)提出的要求就是,必須要非常快,唯有如此才能跟上CPU處理核心的速度。

Side Notes: 為什么在不考慮成本的情況下緩存不是越大越好,也是因為當(dāng)緩存規(guī)模越大,查找某一特定數(shù)據(jù)就會越慢。而緩存首先要滿足的要求就是快,其他的都是次要的。

根據(jù)內(nèi)核的基本知識我們知道,進(jìn)程運行時都有一個虛擬地址「Virtual address」和其所對應(yīng)的物理地址「physical address」。

從虛擬地址到物理地址的翻譯轉(zhuǎn)換也由CPU通過page table完成。Page table并不儲存在CPU里,但近期查找到的Page table entry「PTE」都像數(shù)據(jù)一樣,緩存在了CPU中的translation lookaside buffer「TLB」里。為了不再過多堆砌術(shù)語和名詞,畫張圖說明一下:

當(dāng)CPU根據(jù)程序要求需要讀取某個地址上的數(shù)據(jù)時,首先會在L1 Cache中查找。為了適應(yīng)CPU的速度,L1緩存實現(xiàn)為Virtually indexed physically tagged「VIPT」的形式,即用虛擬地址即可直接讀取該虛擬地址對應(yīng)的物理地址的內(nèi)容,而不再需要多加一道轉(zhuǎn)換的工序。

如果L1 Cache miss,則會在下級緩存中查找。但越過L1 Cache之后,對L2$和L3$的速度要求就不再這么嚴(yán)苛。此時CPU core給出的虛擬地址請求會先通過TLB轉(zhuǎn)換為物理地址,再送入下級緩存中查找。而檢查進(jìn)程有沒有權(quán)限讀取某一地址這一過程,僅在地址轉(zhuǎn)換的時候發(fā)生,而這種轉(zhuǎn)換和檢查是需要時間的,所以有意地安排在了L1 Cache之后。

L1緩存這種必須求“快”的特性,成了整個事件的楔子。

分支預(yù)測

分支預(yù)測是一種提高流水線執(zhí)行效率的手段。在遇到if..else..這種程序執(zhí)行的分支時,可以通過以往的歷史記錄判斷哪一分支是最可能被執(zhí)行的分支,并在分支判斷條件真正返回判斷結(jié)果之前提前執(zhí)行分支的代碼。詳情可以在上面提到的連載文章中閱讀。

需要強調(diào)的是,提前執(zhí)行的分支代碼,即便事后證明不是正確的分支,其執(zhí)行過程中所讀取的數(shù)據(jù)也可以進(jìn)入L1緩存。在Intel的官網(wǎng)文檔《Intel? 64 and IA-32 Architectures Optimization Reference Manual》第2.3.5.2節(jié)中指:

L1 DCache Loads:
- Be carried out speculatively, before preceding branches are resolved.
- Take cache misses out of order and in an overlapped manner.

Show you the [偽] code:

if (likely(A < B)) { ? ?value = *(kernel_address_pointer);}

當(dāng)分支判斷條件A < B被預(yù)測為真時,CPU會去提前執(zhí)行對內(nèi)核地址的讀取。當(dāng)實際條件為A > B時,雖然內(nèi)核的值不會真正寫入寄存器(沒有retire),但會存入L1 Cache,再加之上一節(jié)介紹的,獲取L1 Cache的值毋須地址轉(zhuǎn)換,毋須權(quán)限檢查,這就為內(nèi)核信息的泄漏創(chuàng)造了可能。

從理論上來講,如果可以控制程序的分支判斷,并且可以獲取L1緩存中的數(shù)據(jù)(這個沒有直接方法,但可以通過其他間接手法)的話,就完全可以獲取內(nèi)核信息。而分支預(yù)測這種特性是不能隨隨便便就關(guān)閉的,這也就是這次問題會如此棘手的原因。

亂序執(zhí)行

還有一個原因是亂序執(zhí)行,但原理大致類似。亂序執(zhí)行是Intel在1995年首次引入Pentium Pro處理器的機(jī)制。其過程首先是將我們在匯編代碼中看到的指令“打散”,成為更細(xì)粒度的微指令「micro-operations」,更小的指令粒度將會帶來更多的亂序排列的組合,CPU真正執(zhí)行的是這些微指令。

沒有數(shù)據(jù)依賴的微指令在有相應(yīng)執(zhí)行資源的情況下亂序并行執(zhí)行,進(jìn)而提升程序的并行程度,提高程序性能。但引入的問題是,讀取內(nèi)核數(shù)據(jù)的微指令可能會在流水線發(fā)出exception之前將內(nèi)核數(shù)據(jù)寫入L1 Cache。與分支選擇一樣,為通過用戶態(tài)進(jìn)程獲取內(nèi)核代碼提供了可能。

限于篇幅,更詳細(xì)的內(nèi)容讀者可以在國外安全團(tuán)隊發(fā)布的消息中獲取。

后續(xù)

剛剛查閱之前連載中的一些細(xì)節(jié)的時候,看到在“流水線”那一章里寫過這樣一段話:

在面對問題的時候,人總是會傾向于引入一個更復(fù)雜的機(jī)制來解決問題,多級流水線就是一個例子。復(fù)雜可以反映出技術(shù)的改良,但“復(fù)雜”本身就是一個新的問題。這也許就是矛盾永遠(yuǎn)不會消失,技術(shù)也不會停止進(jìn)步的原因。但“為學(xué)日益,為道日損”,愈發(fā)復(fù)雜的機(jī)制總會在某個時機(jī)之下發(fā)生大破大立,但可能現(xiàn)在時機(jī)還沒有到來:D

很難講現(xiàn)在是不是就是所謂的那個“時機(jī)”。雖然對整個行業(yè)都產(chǎn)生了負(fù)面影響,但我對此仍保持樂觀。因為這就是事物自然發(fā)展的一個正常過程。性能損失并不是一件壞事,尤其是對牙膏廠的用戶來說。

◆◆◆

作者簡介

張攀

一個不耽誤碼字的網(wǎng)工

張攀,云杉網(wǎng)絡(luò)工程師,專注于x86網(wǎng)絡(luò)軟件的開發(fā)與性能優(yōu)化,深度參與ONF/OPNFV/ONOS等組織及社區(qū),曾任ONF測試工作組副主席。

編輯于 2018-01-05 14:12

漏洞

安全漏洞


評論千萬條,友善第一條

6 條評論

默認(rèn)

最新

驍龍800

講了一大堆教科書上的東西,就是沒講漏洞本身

2018-01-08

呵呵忙

這個回答看著好像是要解釋漏洞,結(jié)果只是裝模作樣地科普了一番

2018-01-09

知乎用戶0nC45a

性能損失怎么就不是壞事了?

2018-01-12

打王者被ELO制裁

Spectre存在于一切有亂序執(zhí)行的現(xiàn)代處理器架構(gòu)里面,包括AMD

還有mips芯片龍芯呢,難道不是亂序執(zhí)行的架構(gòu)么

2018-01-08

xingshi he

坐等更新

2018-01-08

Sartner

下次擠牙膏恐怕要擠一大坨

2018-01-07


并發(fā)編程:亂序執(zhí)行的那些事兒的評論 (共 條)

分享到微博請遵守國家法律
浦北县| 杭锦旗| 偃师市| 千阳县| 黔南| 临桂县| 晋城| 朝阳县| 密云县| 抚顺县| 合水县| 平和县| 彰武县| 临夏市| 安福县| 广元市| 那曲县| 库车县| 白城市| 贡嘎县| 绥宁县| 屯门区| 和龙市| 安化县| 巩留县| 长葛市| 连平县| 六盘水市| 黔东| 栖霞市| 麟游县| 彭泽县| 陆河县| 荔波县| 大荔县| 邵阳县| 揭阳市| 赤水市| 广昌县| 武平县| 无为县|