優化自旋鎖的實現

在《C++11實現一個自旋鎖》介紹了分別使用TAS和CAS算法實現自旋鎖的方案,以及它們的優缺點。TAS算法雖然實現簡單,但是因為每次自旋時都要導致一場內存總線流量風暴,對全局系統影響很大,一般都要對它進行優化,以降低對全局系統的負面影響,本文討論一下它的優化方案。

下面是使用atomic_flag原子類+TAS算法實現的自旋鎖代碼。

class spin_lock final {atomic_flag lock_var{false}; // 鎖變量
public:void lock() {while (lock_var.test_and_set(memory_order_acquire));}void unlock() {lock_var.clear(memory_order_release);}
};

從上面的代碼可以看到,在調用lock()函數申請鎖時,只要鎖變量lock_var的值不是false,就會無條件的循環執行test_and_set(),執行的太頻繁了,以至于其它CPU的運行都要受到影響。因為一個CPU在test_and_set()執行時要原子事務寫lock_var變量到內存中,每執行一次就導致一場內存總線流量風暴,其它CPU要訪問內存就得競爭內存總線。如果競爭不到,得停下來等待內存總線的釋放,造成CPU一定程度上處于“失速”狀態。如果幾個線程同時在申請鎖,每一次TAS的原子事務寫內存,都會加劇內存總線的競爭程度。

如何進行優化呢?

雙檢查機制

在實現單例模式時,我們知道有一種針對互斥鎖的優化方案,叫做雙檢查鎖機制,它的基本思想是先使用執行成本低的代碼來檢查條件是否滿足,如果條件滿足,再使用執行成本高的代碼,即互斥鎖的方式去檢查條件。

可以借鑒這個思想來對TAS算法進行優化,基本思路是在調用TAS之前,先讀取鎖變量,此過程是只讀內存操作,相比RMW操作是輕量級的操作,沒有全局影響和副作用,執行成本較低。然后對讀取的值進行判斷,如果是true,則繼續循環重復此過程,直到返回false,說明有線程已經釋放鎖了,此時退出循環,再進行重量級的TAS操作。也就是僅在必要的時候才執行TAS操作,這樣,即使TAS操作導致的全局影響很大,但是它發生的次數卻大為降低了。

因為C++11版的atomic_flag原子類沒有提供讀取原子變量的接口,直到C++20版本才提供了成員函數test(),它返回原子變量的值,可以使用這個接口來實現雙檢查操作。下面是修改后的lock()函數代碼:

void lock() {while (true) {// 第一次檢查:使用Test操作while (lock_var.test(memory_order_relaxed));// 第二次檢查并設置:TAS操作if (!lock_var.test_and_set(memory_order_acquire)) {break; // 成功獲得鎖,跳出循環}}
}

與前面的實現相比,多了一個test()循環調用,同時原來的test_and_set()循環調用變成了單次調用,只有在test()檢測到lock_var為false時退出循環之后,才會進行test_and_set()調用。

循環代碼“while (lock_var.test(memory_order_relaxed))”就是上面說的第一次檢查操作,調用atomic_flag::test()接口返回lock_var的值,這里沒有內存順序的需要,因此使用了最松散的內存序,如果返回值是true,則繼續循環,如果是false,則退出循環,立刻進入第二次檢查,這時為什么還要進行檢查呢?因為第一次檢查不能原子地設置lock_var為true,所以還得要使用TAS操作,由于已經檢測到lock_var為false了,這次執行TAS成功的幾率非常高。

由此可見,該方式在自旋鎖中的實現邏輯是Test-Test-And-Set,所以也簡稱為TTAS算法。

為了查看TTAS的實現細節,我們看一下它的匯編指令代碼,下面是使用-std=c++20編譯選項,生成的匯編指令:

spin_lock::lock():mov     edx, 1
.L17:
// 從內存中讀取鎖變量的值movzx   eax, BYTE PTR [rdi]
// 判斷是否是0,即false,第一次檢查 Testtest    al, al
// 如果不是0,則回退jne     .L17mov     eax, edx
// 第二次檢查并設置:TAS操作xchg    al, BYTE PTR [rdi]test    al, aljne     .L17ret

看第一次檢查(第3-9行),在每次檢查時都要從內存[rdi]處讀取變量lock_var,然后檢查是否為0,即false,如果不為0,則繼續退回去重新讀取再檢查,一直循環執行“讀取-判斷-回退”的邏輯,直到讀出的lock_var為0為止。注意,這里的讀取指令是“movzx eax, BYTE PTR [rdi]”,是非常簡單的指令,它從內存中加載一個字節的數據到寄存器al中,也是一條原子操作指令,但該指令不會鎖總線。根據MESI緩存一致性協議,如果lock_var的值沒有被別的CPU修改過,那么它就在當前CPU的L1 cache緩存中會一直有效,也就是只要內存中的lock_var的值沒有被更新,該指令執行時就會一直從L1 cache中讀取,即沒有訪問內存,也就不會占用內存總線,不會影響其它CPU,只在本CPU中循環,所以第一次的循環檢查也被稱為“本地自旋”(見匯編指令代碼第5(讀取)、7(判斷)、9(回退)行)。

1、提高了整體吞吐量性能

同TAS算法相比,TTAS算法大大減輕了對內存總線的競爭程度,也降低了TAS執行時為了維持cache一致性而產生的內存總線流量。使用《利用C++11原子操作實現自旋鎖》中的測試代碼,所測試的TTAS以及TAS、CAS算法的性能數據如下:

TTAS所用時鐘周期        TAS所用時鐘周期      CAS所用時鐘周期
337427552              432447222           177915346
394388223              502814284           279384821
390494013              556108365           227520077
401288104              513192991           346827330

可見,在同樣的測試條件,TTAS算法要比TAS算法性能要好,但是仍然比CAS算法要差。

2、增大了個體CPU申請鎖時延遲的可能性

TAS算法實現自旋鎖時,如果返回了false同時也就獲得了鎖,檢查和設置是一個原子操作,而使用TTAS算法時要對鎖變量進行兩次檢查判斷,第一次檢查返回false時,只是有可能會獲得鎖,能否獲得還得要使用后面的TAS算法。因此,使用TTAS算法實現,申請鎖的速度沒有TAS算法快,也就是申請鎖時的延遲要比TAS算法大。

因此,可以把Test和TAS的位置調換一下,先進行TAS算法,當一個線程剛進入lock()時,如果鎖是釋放狀態,可能會馬上通過TAS來獲得鎖,而不用等到從Test循環中退出,再使用TAS來競爭鎖,能降低一點申請鎖的延遲,當然如果第一次沒有競爭到鎖,就進入了本地自旋,等待lock_var變為false,仍然還是存在延遲。代碼如下:

void lock() {while (true) {// 第一次檢查并設置:TAS操作if (!lock_var.test_and_set(memory_order_acquire)) {break; // 成功獲得鎖,跳出循環}// 第二次檢查:使用Test操作while (lock_var.test(memory_order_relaxed));}
}

3、TTAS是比TAS更“忙”的算法

在TTAS算法中,第一個Test檢查過程是本地自旋,它不同于TAS自旋,它的性能更高,執行的速度更快:

首先,先看指令部分,是個微小循環,可以被CPU的循環流檢測器(識別和鎖定微操作指令隊列中小的循環)檢測到,并放入trace-cache中,可以在下一次循環時直接使用。下圖是它的指令流截圖,非常短小精悍,僅有三行指令,指令長度是17-10=7個字節度,被譯碼單元譯碼后的微操作碼應該也不多(其中jne指令和它前面的test指令還會被宏融合為單一的微操作碼),它們存放在CPU的trace-cache里面肯定綽綽有余,注意里面存放的是已經解碼完的微操作碼,可以被執行引擎直接派發和執行。也就是說對于這個本地自旋小循環,不但不需要從指令cache中獲取指令,而且連指令解碼過程也被省掉了,此時不會因為程序流程發生跳轉了,需要重新從指令cache中加載指令和譯碼的過程,導致CPU出現流水線的中斷。

圖片

再看數據部分,因為對鎖變量數據lock_var是一個只讀操作,此時作為熱點數據都在L1 cache中,讀取時不需要從內存中獲取,速度極快,因此,在本地自旋時,也不會出現因為需要加載數據導致CPU流水線中斷的情況。此外,因為這個本地自旋運行次數很多,CPU的分支預測單元會發揮作用,知道下一步要跳轉到第10行去執行,CPU就會預測執行,這樣這三條指令就可以并行執行,即test指令在判斷當前lock_ver值的同時,又在讀取下一次的lock_var值為下一次test指令做準備,這是指令級的并行化(因為有寄存器重命名機制,第10行更新eax(al是它的低8位)寄存器和第13行測試al寄存器不會沖突,這個寫后讀是假依賴,可以同時執行)。

也就是說,當CPU處于本地自旋時,無論是指令還是數據,它們都處于CPU的內部,不會發生因為cache miss而中斷CPU流水線去等待它們,而且還可以進行指令級的并行化,會一直處于高速運轉中,也就是它更“忙”了,當然副作用就是耗電量非常大,這是TTAS算法不好的一面。

4、一旦進入本地自旋,大概率是要空轉

再看TTAS算法的代碼實現,當第一次檢查時,如果一個CPU從本地自旋中退出,說明此時已經有別的CPU把這個lock_var變量置為false了,即別的線程已經把鎖釋放了。當接著使用TAS操作進行第二次檢查時,如果沒有設置成功,說明在第一次檢查結束和第二次檢查開始之間出現了狀況:第一種可能它是和別的CPU在執行時間上有重疊,但別的CPU搶先獲得了鎖;第二種可能是執行流程被中斷后又返回了,即有中斷或者優先級更高的線程把它搶占了,這個CPU也就暫時沒有參與自旋鎖的競爭。

我們現在討論第一種情況,別的CPU搶先獲得了鎖,然后進入臨界區進行業務處理,這也暗示著自旋鎖不會馬上被釋放,那么本CPU肯定會再次進入“本地自旋”并且會持續一段時間。我們不妨估算一下大概需要自旋多長時間:如果想讓CPU從本地自旋中退出,至少要等到另一個CPU執行完臨界區代碼后,調用unlock把false值寫入內存變量lock_var,本CPU讀取lock_var變量時發現緩存和內存中的值不一致,重新從內存中加載;假設臨界區的最小執行時間為t,也就是說最少也要等待CPU一次原子事務寫內存的時間,加上一次讀內存的時間,再加上t,小于這個時間是不會從循環中退出的,這段時間CPU一直處于空轉狀態。

也就是說,只要進入本地自旋,大概率是要一直空轉的,一直到別的線程執行完臨界區代碼釋放鎖后才有可能停止空轉。那么,這個空轉既然避免不了,在這期間CPU有什么優化措施嗎?

x86的pause指令

現代CPU大多提供了超線程技術(即邏輯CPU),即在一個物理核上有兩個邏輯核,它們有自己獨享的寄存器,但是需要共享執行單元、系統總線和緩存等硬件資源。如果執行自旋操作的CPU是一個邏輯核,當它進入本地自旋的時候,會加劇物理核中的執行資源和系統總線的競爭程度,從而影響其它同一個邏輯核的執行效率。

我們再分析一下TTAS的lock代碼的結構特點:代碼雖然不多,但是嚴重同質化,有兩次數據傳輸操作,兩次test操作,一次比較交換操作,也就是它們使用相同執行單元的可能性非常大。如果處于自旋的兩個線程剛好被分配到同一個物理核的兩個邏輯核上運行,兩個邏輯核共享執行單元和系統總線,它們在自旋操作時可能會遭遇資源沖突,反而導致整體性能下降。

為了改善這種情況,Intel開發者手冊建議使用pause指令優化。intel的CPU針對自旋等待的場景提供了一個指令:pause,該條指令的功能是給CPU一個提示:當前正在進行自旋等待,可以暫停一下,比如等待一個或幾個內存總線操作周期的時間之后,然后再繼續運行(因為在這期間,CPU是大概率獲取不到自旋鎖的,與其讓CPU自旋忙等,不如停下來休息一下)。因此,一個邏輯核進入了pause狀態,也就不再使用系統資源了,這樣,同物理核的另一個邏輯核就可以獨享物理核的全部資源了,提高了申請鎖時響應速度,也降低了CPU的電量消耗。

因為C++中沒有相應的函數來執行pause指令,可以在代碼中嵌入匯編指令,下面是基于TTAS算法修改后的代碼(同樣也可以使用pause指令優化基于CAS算法實現的自旋鎖):

void lock() {while (true) {if (!lock_var.test_and_set(memory_order_acquire)) {break;}while (lock_var.test(memory_order_relaxed)) {asm("pause");}}
}

這情況下能夠大大降低CPU執行資源和電量的消耗。不過,這種方法會進一步增加獲取鎖的延遲性,因為每次自旋條件不符合時就得先pause一段時間,如果在pause期間,鎖被釋放了,也不會及時的獲取鎖,只能等到CPU從pause中退出時才有可能。而且這個延遲的時間也不固定:有可能CPU剛剛開始pause,鎖就釋放了,顯然此時延遲最大;也有可能CPU從pause退出時,恰好鎖就釋放了,顯然此時延遲最小,這是最理想的場景。如果應用場景是對響應性能敏感的場景,就得要慎重考慮了,能否容忍使用pause指令帶來的延遲損失。

同時我們也知道,當CPU處于pause狀態時,什么也不做,不像操作系統提供的yield命令,可以讓CPU執行別的任務。讓CPU資源處于閑置狀態,可能會感覺有點可惜,不過在支持超線程技術的CPU架構體系中可以利用這個特點,在一定程度上緩解邏輯核的資源競爭問題,提高超線程的執行速度,因為一個邏輯核執行pause而處于暫停狀態,也在一定程度上緩解了CPU的耗電問題。

不過需要指出的是,當自旋鎖保護的臨界區執行時間很短時,比如只是簡單的幾個內存訪問和計算,可以使用pause機制;如果自旋鎖保護的臨界區執行時間很長時,或者一個線程獲得自旋鎖后接著被搶占了,使用pause機制會讓其它申請自旋鎖的CPU長時間處于暫停狀態,也是一種資源浪費。顯然這種情況下不如把CPU分配給別的線程使用,此時涉及到線程被調度的情況了,就得考慮讓操作系統參與進來了,比如調用yield,把CPU調度給別的任務,或者直接把當前線程掛起,等到解鎖時再喚醒它。

總結

1、TAS算法實現自旋鎖,會導致內存總線流量風暴,全局系統影響大。

2、TTAS雖然抑制了流量風暴的產生,減輕了全局內存總線的競爭程度,但是又導致CPU耗電量大、發熱等情況。

3、使用pause指令緩解了超線程核心的系統資源競爭和降低了耗電量,但是又增長了獲取鎖時的延遲。

4、TTAS算法和pause指令是在程序和CPU上面對自旋鎖的優化,如果獲取不到鎖時,線程仍然處于自旋中,不會發生調度和阻塞現象,沒有改變自旋鎖的本質特征。

參考:

1、https://zh.cppreference.com/w/cpp/atomic/atomic_flag/test

2、Intel? 64 and IA-32 Architectures Software Developer’s Manual

3、《現代x86匯編語言程序設計》 丹尼爾-卡斯沃姆

附錄:

下面內容是Intel手冊對pause指令的描述。

PAUSE—Spin Loop Hint
Improves the performance of spin-wait loops. When executing a “spin-wait loop,” processors will suffer a severe performance penalty when exiting the loop because it detects a possible memory order violation. The PAUSE instruction provides a hint to the processor that the code sequence is a spin-wait loop. The processor uses this hint to avoid the memory order violation in most situations, which greatly improves processor performance. For this reason, it is recommended that a PAUSE instruction be placed in all spin-wait loops.

An additional function of the PAUSE instruction is to reduce the power consumed by a processor while executing a spin loop. A processor can execute a spin-wait loop extremely quickly, causing the processor to consume a lot of power while it waits for the resource it is spinning on to become available. Inserting a pause instruction in a spinwait loop greatly reduces the processor’s power consumption.

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/77440.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/77440.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/77440.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Excel 中讓表格內容自適應列寬和行高

Excel 中讓表格內容自適應列寬和行高 目錄 Excel 中讓表格內容自適應列寬和行高自適應列寬自適應行高在Excel中讓表格內容自適應列寬和行高,可參考以下操作: 自適應列寬 方法一:手動調整 選中需要調整列寬的列(如果是整個表格,可點擊表格左上角行號和列號交叉處的三角形全…

JWT令牌:實現安全會話跟蹤與登錄認證的利器

摘要:本文深入探討了JWT令牌在實現會話跟蹤和登錄認證方面的應用,詳細介紹了JWT令牌的概念、組成、生成與校驗方法,以及在實際案例中如何通過JWT令牌進行會話跟蹤和登錄認證的具體實現步驟,為系統的安全認證機制提供了全面且深入的…

Mybtis和Mybatis-Plus區別

MyBatis 和 MyBatis-Plus 是 Java 中常用的持久層框架,MyBatis-Plus 是在 MyBatis 基礎上增強的工具包,讓開發更便捷、高效。下面是兩者主要的區別: ? 核心區別總結: 特性MyBatisMyBatis-Plus配置復雜度需要手寫大量 XML 或注解…

JavaScript 性能優化實戰

一、代碼執行效率優化 1. 減少全局變量的使用 全局變量在 JavaScript 中會掛載在全局對象(瀏覽器環境下是window,Node.js 環境下是global)上,頻繁訪問全局變量會增加作用域鏈的查找時間。 // 反例:使用全局變量 var globalVar = example; function someFunction() {con…

學習筆記十六——Rust Monad從頭學

🧠 零基礎也能懂的 Rust Monad:逐步拆解 三大定律通俗講解 實戰技巧 📣 第一部分:Monad 是什么? Monad 是一種“包值 鏈操作 保持結構”的代碼模式,用來處理帶上下文的值,并方便連續處理。 …

PL/SQL登錄慢,程序連接Oracle 提示無法連接或無監聽

PL/SQL登錄慢,程序連接Oracle 提示無法連接或無監聽 錯誤提示:ORA-12541: TNS: 無監聽程序 的解決辦法, 現象:PL/SQL登錄慢,程序連接Oracle 提示無法連接或無監聽 監聽已經正常開起,但還是PL/SQL登錄慢或…

Windows10,11賬戶管理,修改密碼,創建帳戶...

在這里,我們使用微軟操作系統的一款工具:netplwiz 它可以非常便捷的管理用戶賬戶. 一:修改密碼(無需現在密碼) 01修改注冊表 運行命令:regedit 在地址欄輸入: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Passwor…

電腦 BIOS 操作指南(Computer BIOS Operation Guide)

電腦 BIOS 操作指南 電腦的BIOS界面(應為“BIOS”)是一個固件界面,允許用戶配置電腦的硬件設置。 進入BIOS后,你可以進行多種設置,具體包括: 1.啟動配置 啟動順序:設置從哪個設備啟動&#x…

iOS 冷啟動時間監控:啟動起點有哪些選擇?

?? iOS 冷啟動時間監控:啟動起點有哪些選擇? 作者:侯仕奇 來源:sqi.io 在監控 iOS 冷啟動性能時,一個關鍵問題是:如何精確記錄 App 冷啟動的開始時間? 本文將對不同的“冷啟動起點”監控方式…

onlyoffice關閉JWT后依然報錯如何解決?

onlyoffice關閉JWT后依然報錯如何解決? 一、部署方式 我是以docker方式部署的,直接通過環境變量禁用了JWT,命令如下: docker run -d \--name onlyoffice-no-jwt \--restartalways \-p 8069:80 \-e JWT_ENABLEDfalse \onlyoffic…

rk3588 驅動開發(一)字符設備開發

3.字符設備驅動開發 3.1 什么是字符設備驅動 字符設備:就是一個個字節,按照字節流進行讀寫操作的設備,讀寫是按照先后順序的。 舉例子:IIC 按鍵 LED SPI LCD 等 Linux 應用程序調用驅動程序流程: Linux中驅動加載成功…

設計模式 --- 外觀模式

外觀模式是一種結構型設計模式,為復雜子系統提供??統一的高層接口??,通過定義一個外觀類來??簡化客戶端與子系統的交互??,降低系統耦合度。這種模式隱藏了子系統的復雜性,將客戶端與子系統的實現細節隔離開來,…

我的gittee倉庫

日常代碼: 日常代碼提交https://gitee.com/xinxin-pingping/daily-code 有需要的寶子們可自行讀取。

微服務調用中的“大對象陷阱”:CPU飆高問題解析與優化

背景 對幾十萬條用戶歷史存量數據寫入,且存在大對象的基礎上。kafka消費進行消費寫mysql超時。導致上游服務調用時異常,CPU飆高異常。 大對象解釋 大對象的定義與危害 1. 什么是大對象? JVM 內存分配機制:Java 中對象優先分配…

代碼隨想錄算法訓練營day6(字符串)

華子目錄 反轉字符串思路 反轉字符串II思路 替換數字思路 反轉字符串 https://leetcode.cn/problems/reverse-string/ 思路 使用雙指針&#xff0c;初始化時&#xff0c;left指向下標0的位置&#xff0c;right指向最后一個元素的下標當while left<right時&#xff0c;交換…

Oracle 19c新特性:OCP認證考試與職業躍遷的關鍵?

在數字化轉型的浪潮中&#xff0c;Oracle 19c作為數據庫領域的旗艦版本&#xff0c;不僅承載著技術革新的使命&#xff0c;更成為IT從業者職業進階的“黃金跳板”。無論是企業級應用的高可用性需求&#xff0c;還是云原生架構的快速迭代&#xff0c;Oracle 19c的智能化與多模型…

【MySQL數據庫入門到精通】

文章目錄 一、SQL分類二、DDL-數據庫操作1.查詢2.創建數據庫3.刪除數據庫4.使用數據庫 三、DDL-表操作1.查詢 一、SQL分類 根據功能主要分為DDL DML DQL DCL DDL:Date Definition Language數據定義語言&#xff1a;定義數據庫&#xff0c;表和字段 DML:Date Manipulatin Lan…

MCP服務端開發

MCP(Memory, Context, Planning)是一種增強AI系統認知能力的框架,通過整合記憶管理、上下文理解和規劃能力,可以顯著提升AI系統的表現。下面我將為您開發一個完整的MCP服務端。 概述 我們將使用Python開發一個基于FastAPI的MCP服務端,包含以下核心組件: Memory Manager…

前端:uniapp中uni.pageScrollTo方法與元素的overflow-y:auto之間的關聯

在uniapp中&#xff0c;uni.pageScrollTo方法與元素的overflow-y:auto屬性之間存在以下關聯和差異&#xff1a; 一、功能定位差異 ?uni.pageScrollTo? 屬于?頁面級滾動控制?&#xff0c;作用于整個頁面容器?34。要求頁面內容高度必須超過屏幕高度&#xff0c;且由根元素下…

基礎知識-指針

1、指針的基本概念 1.1 什么是指針 1.1.1 指針的定義 指針是一種特殊的變量&#xff0c;與普通變量存儲具體數據不同&#xff0c;它存儲的是內存地址。在計算機程序運行時&#xff0c;數據都被存放在內存中&#xff0c;而指針就像是指向這些數據存放位置的 “路標”。通過指針…