AMPERE ALTRA?和?AMPERE ALTRA MAX 的鎖機(jī)制
讓我們先來了解一些基本的問題。Arm 在 Arm?v8.2-A 架構(gòu)中引入了大型系統(tǒng)擴(kuò)展(Large System Extensions, LSE),它用單個原子指令取代了鎖操作的指令序列。
一個非常不錯的總結(jié)。雖然舊的 Arm 版本在功能上可以很好地工作,但隨著核心數(shù)量的增加和鎖的爭用更加頻繁,預(yù)計性能會受到影響。
Ampere Altra 和 Ampere Altra Max 支持 LSE,并配備了可擴(kuò)展的鎖性能。
為了說明使用的指令之間的差異,讓我們看看 gcc 的處理方式
__atomic_fetch_add()。在本例中,將鎖值減 1:
?
__atomic_fetch_add(&lockptr->lockval, -1, __ATOMIC_ACQ_REL);
?
使用* -march =armv8.2-a*選項編譯,編譯器生成帶有原子指令的代碼:
?
998: ? f8f60280 ? ? ? ?ldaddal x22, x0, [x20]
?
另一方面,設(shè)置* -march =armv8-a*(不支持LSE),生成一個不同的序列:
?
9a4: ? c85ffe60 ? ldaxr ? x0, [x19] 9a8: ? d1000400 ? sub ? ? x0, x0, #0x1 9ac: ? c801fe60 ? stlxr ? w1, x0, [x19] 9b0: ? 35ffffa1 ? cbnz ? ?w1, 9a4
?
為了使序列具有原子性,需要一個單獨的監(jiān)視器。ldaxr 獲得一個地址標(biāo)記,在本例中為 [x19]。然后執(zhí)行減法,然后存儲回內(nèi)存位置。
但是,只有當(dāng)存儲(store)時的標(biāo)記與加載(Load)中的標(biāo)記匹配時,存儲才會成功。stlxr 之后的條件分支 cbnz 檢查存儲是否成功,這意味著 load 和 store 中的標(biāo)記匹配。
如果不是,則跳回序列的開頭,在本例中是地址 0x9a4。
這里值得注意的是,如果沒有 LSE 指令,這個指令序列可能要執(zhí)行幾次才能被認(rèn)為成功。使用 LSE, ldaddal 指令可以保證以一條指令完成,不需要循環(huán)。
圖 1 顯示了當(dāng)線程數(shù)從 1 增加到 80 時,使用 LSE 和不使用 LSE 時每秒獲得排他鎖的性能差異。
圖 1
通常,Compare 和 Exchange 硬件指令用于在軟件中實現(xiàn)鎖。需要注意的是,這些指令必須是原子指令。
原子在這里是什么意思呢?這些指令首先獲得包含鎖的緩存行(Cache Line)的所有權(quán),并將其加載到 CPU 的本地緩存中。然后將當(dāng)前值與隨指令提交的比較值進(jìn)行比較。
如果相等,作為指令一部分提交的新值將替換當(dāng)前值。如果不相等,則保持當(dāng)前值。這方面的原子性意味著整個序列由一個線程執(zhí)行,而沒有其他線程訪問緩存行,由硬件保證。
鎖的種類
在軟件中可以實現(xiàn)不同類型的鎖,如互斥鎖(mutexes)、票據(jù)鎖(ticket)和自旋鎖(spinlocks)。如前所述,不同的鎖類型在軟件中實現(xiàn),硬件提供類似 cmpxchg 或 fetchadd 的指令。相同的鎖類型在不同的硬件上運行,只有使用的指令不同。
如何實現(xiàn)鎖機(jī)制
這是一個非常重要的問題。讓我們把它分解成兩個選項:1) 使用可用的庫和 2) 使用原子指令來實現(xiàn)專有的鎖定算法。
選項1有幾個優(yōu)點。庫已經(jīng)存在,不需要自定義實現(xiàn),而且經(jīng)過了充分測試,通常將會在未來的庫版本中進(jìn)行維護(hù)。例如?pthread_mutex_lock?和pthread_rwlock。
聽起來不錯,那么有什么缺點呢? 缺乏統(tǒng)計數(shù)據(jù)可能是一個問題。沒有向應(yīng)用程序返回任何信息,報告旋轉(zhuǎn)(spins)或線程被調(diào)度出多少次。此外,庫實現(xiàn)可能不太適合某些應(yīng)用程序,因為庫更通用。
選項 2 更復(fù)雜。它需要實現(xiàn)鎖定函數(shù)并維護(hù)它們。但是,它可以獲得一些好處,因為它是專門為應(yīng)用程序設(shè)計的。鎖定原語和原子指令可以通過內(nèi)聯(lián)匯編(inline assembly)或利用編譯器的支持來實現(xiàn)。同樣,使用內(nèi)聯(lián)程序集編寫代碼需要應(yīng)用程序維護(hù)該段代碼。
對于編譯器,gcc提供了atomic built-in function,它允許應(yīng)用程序使用低級函數(shù),這些函數(shù)將被編譯成 Arm 原子指令。這些內(nèi)置函數(shù)為應(yīng)用程序提供了原子指令和內(nèi)存序指令的不同方法。代碼也更易于移植。但是,使用*-mcpu或-march*的正確設(shè)置來編譯應(yīng)用程序來生成 Arm LSE 指令是很重要的。Ampere Altra 和 Ampere Altra Max 使用 Neoverse-n1 架構(gòu),其中就包括LSE。
然而,使用原子指令實現(xiàn)鎖需要設(shè)計決策。如果鎖被持有,旋轉(zhuǎn)(spinning)是否合理?轉(zhuǎn)幾圈?線程在旋轉(zhuǎn)一定次數(shù)后如果不成功,是否應(yīng)該放棄?在旋轉(zhuǎn)環(huán)中需要后退,還是直線旋轉(zhuǎn)? 這些只是需要解決的問題中的一部分。
其他的設(shè)計決策
1
鎖的數(shù)據(jù)類型和大小
通常,應(yīng)用程序使用 int 或long 作為鎖。用于原子操作的內(nèi)置函數(shù)(Built-in functions)從內(nèi)存中讀取鎖值。如果應(yīng)用程序也直接讀取鎖值,鎖類型應(yīng)該有“volatile”前綴,例如 volatile long。使用 volatile,編譯器生成從內(nèi)存中讀取數(shù)據(jù)的指令。否則,該值可能在寄存器中而沒有更新,從而錯過對鎖位置的更新。
2
鎖的粒度
由于競爭,粗粒度鎖有可能成為性能瓶頸。另一方面,如果每個資源都有自己的鎖來保護(hù),那么將需要大量內(nèi)存來存儲鎖。必須是一種折衷設(shè)計,以避免任何不利因素。
3
鎖對齊
編譯器對結(jié)構(gòu)進(jìn)行正確對齊。如果應(yīng)用程序管理自己的內(nèi)存,那么鎖的位置可能與鎖的大小不一致。在最壞的情況下,鎖可能跨越兩條緩存行。在 AArch64 上,對未對齊鎖的原子操作會導(dǎo)致 SIGBUS (硬件向操作系統(tǒng)發(fā)出信號,表明 CPU 不能尋址內(nèi)存地址的總線錯誤,在這種情況下是由于未對齊訪問)。從積極的方面來看,獲得 SIGBUS 需要固定對齊,而不是隱藏很少被發(fā)現(xiàn)的性能問題。
4
假共享
虛假分享是什么意思?即同一高速緩存行上的獨立數(shù)據(jù)對性能有不良影響,鎖數(shù)組就屬于這一類。這些鎖保護(hù)不同的關(guān)鍵區(qū)域。但是,對同一緩存行上鎖的原子操作會影響該緩存行上的所有鎖。重要的是,原子性不是針對鎖本身,而是針對包含鎖的整個緩存行。
5
在 cmpxchg 之前做測試
在執(zhí)行 cmpxchg 指令之前讀取自旋循環(huán)(spinloop)中的鎖值可能對爭用鎖有利。Cmpxchg 需要緩存行的所有權(quán),而test將以共享模式獲取緩存行,從而避免失效。然而,這可能會增加執(zhí)行的 spin 數(shù)量。
6
如果可能的話,在無鎖時
使用?fetchadd 而不是 cmpxchg
釋放鎖需要返回線程為獲取鎖而執(zhí)行的操作。Cmpxchg,特別是對于共享鎖或讀寫鎖,需要一個循環(huán),并且由于鎖值的變化而可能會重試操作。然而,fetchadd 不需要循環(huán),沒有比較,因此它會成功。
7
鎖定持有時間
通常指臨界區(qū)域內(nèi)的指令數(shù)或在臨界區(qū)域內(nèi)花費的時間。時間是一個更好的度量標(biāo)準(zhǔn),因為臨界區(qū)域可能只有很少的指令。然而,所有的指令都可以從內(nèi)存中讀取。嵌套鎖屬于同一類別。無法獲得內(nèi)部鎖以及 spinning 或 sleeping 會影響外部鎖的保持時間。減少關(guān)鍵區(qū)域的保持時間總是好的,如果數(shù)據(jù)在本地緩存而不是內(nèi)存中就更好了。
8
搶占
不幸的是,線程在持有鎖時可能會被重新調(diào)度。如果鎖處于獨占模式,這意味著沒有其他線程能夠獲得鎖。在考慮性能問題時,要記住這一點。較短的保持時間將降低搶占的可能性。
內(nèi)存序
如前所述,正確的內(nèi)存排序指令對于正確性很重要。AArch64 遵循一種寬松的內(nèi)存模型。使用 LSE, AArch64 指令強(qiáng)制執(zhí)行特定的內(nèi)存順序。例如,cmpxchg 指令集有獲取(CASA 指令)、釋放(CASL 指令)以及獲取和釋放(CASAL 指令)的版本。硬件保證這些指令遵循其特定指令的內(nèi)存模型。這取決于軟件使用適當(dāng)?shù)闹噶睢?br />
通常,acquire 用于鎖獲取,Release 用于鎖釋放。但是,如果應(yīng)用程序在無鎖之后讀取數(shù)據(jù)(例如,如果該鎖有任何等待程序),那么空閑程序的 release 語義可能會導(dǎo)致問題,因為對等待程序結(jié)構(gòu)的讀取可能會提升到空閑程序之上,因此在空閑程序之后,寄存器中就會出現(xiàn)陳舊的數(shù)據(jù)。在這些情況下,最好使用 acquire 和 release 語義。同樣,這取決于應(yīng)用程序?qū)崿F(xiàn)。gcc 編譯器直接在內(nèi)置函數(shù)中使用這些指令。
總結(jié)
Ampere 系列處理器正以其持續(xù)增加的核心數(shù)量不斷挑戰(zhàn)性能極限,并具備使用鎖的多線程應(yīng)用程序可擴(kuò)展性的所有要素。使用 LSE,在硬件中提供原子指令以獲得更好的鎖性能。正如我們在本文所看到的,應(yīng)用程序開發(fā)人員可以通過鎖庫或正確實現(xiàn)鎖定算法來充分利用這些指令。
審核編輯:劉清
評論
查看更多