go 通過匯編學習atomic原子操作原理

文章目錄

    • 概要
    • 一、原理
        • 1.1、案例
        • 1.2、關鍵匯編
    • 二、LOCK匯編指令
        • 2.1、 LOCK
        • 2.2、 原理
            • 2.2.1、 緩存行
            • 2.2.2、 緩存一致性之MESI協議
            • 2.2.3、lock原理
    • 三、x86緩存發展
    • 四、x86 DMA發展
    • 參考

概要

在并發操作下,對一個簡單的a=a+2的操作都會出錯,這是因為這樣簡單的操作在被CPU執行時,分為三步:

  1. cpu從內存中讀取a的值;
  2. cpu執行a=a+2操作;
  3. cpu將執行結果寫回內存。

在多核環境下,很容易發生,A核和B核同時從內存中讀取a的值,經過各自計算后寫回內存的情況,這也解釋了為什么在并發下,不加鎖的累加操作結果常常比預期值小的原因。

在go語言中,對于有競爭的數據操作,我們常常用sync/atomic標準庫提供的原子操作,經典的比如借助CAS原子操作實現自旋鎖。

原子操作即是進行過程中不能被中斷的操作。針對某個值的原子操作在被某個CPU執行的過程當中,其它CPU絕不會進行其它針對該值的原子操作,也就是說,為了實現這樣的嚴謹邏輯,同一時刻原子操作僅會由一個獨立的CPU執行,其它CPU等待,只有這樣才能夠在并發環境下保證原子操作的絕對安全。

Go語言提供的原子操作都是非侵入式的,我們通過調用sync/atomic標準庫提供的函數,對6種簡單的類型的值進行原子操作,這些類型包括int32、int64、uint32、uint64、uintptr和unsafe.Pointer類型。這些函數提供的原子操作共有5種,分別是增加(Add)、比較并交換(CompareAndSwap)、載入(Load)、存儲(Store)、交換(Swap)。

調試的服務器信息:Centos Linux 7 ,CPU AMD x86_64,Go version 1.24

本文通過在CPU x86_64環境下的匯編分析這些原子操作的原理。

前置知識:
x86系列CPU寄存器和匯編指令
go 通過匯編分析棧布局和函數棧幀

一、原理

本章以原子操作Add為例,通過其匯編逐步去分析。

1.1、案例
package mainimport ("fmt""sync/atomic"
)func main() {a := uint64(1)atomic.AddUint64(&a, 5)a+=5fmt.Println("Add", a)
}
1.2、關鍵匯編
[root@test gofunc]# dlv debug atomic.go
Type 'help' for list of commands.
(dlv) b atomic.go:8
Breakpoint 1 set at 0x4b1b13 for main.main() ./atomic.go:8
(dlv) c
> [Breakpoint 1] main.main() ./atomic.go:8 (hits goroutine(1):1 total:1) (PC: 0x4b1b0e)3: import (4:         "fmt"5:         "sync/atomic"6: )7:
=>   8: func main() {9:         a := uint64(1)10:         atomic.AddUint64(&a, 5)11:         a+=512:13:         fmt.Println("Add", a)
(dlv) disass
Sending output to pager...
TEXT main.main(SB) /home/fpf/server/moa-20241226160534/background/tools/source/go-apps/src/gofunc/atomic.goatomic.go:8     0x4b1b00        493b6610                cmp rsp, qword ptr [r14+0x10]atomic.go:8     0x4b1b04        0f86e8000000            jbe 0x4b1bf2atomic.go:8     0x4b1b0a        55                      push rbp #BP寄存器值入棧atomic.go:8     0x4b1b0b        4889e5                  mov rbp, rsp #保存main函數棧幀基址
=>      atomic.go:8     0x4b1b0e*       4883ec68                sub rsp, 0x68 #申請0x68大小棧內存atomic.go:9     0x4b1b12        48c744241801000000      mov qword ptr [rsp+0x18], 0x1 #令變量a=1atomic.go:10    0x4b1b1b        b905000000              mov ecx, 0x5 #令CX寄存器值等于5atomic.go:10    0x4b1b20        488d542418              lea rdx, ptr [rsp+0x18] #&a操作,即將變量a的地址加載到DX寄存器atomic.go:10    0x4b1b25        f0480fc10a              lock xadd qword ptr [rdx], rcx #atomic.AddUint64原子操作,對變量a的地址進行原子操作,可以看到用的是x86_64 cpu提供的lock和xadd匯編指令配合,xadd進行加法操作,lock對變量a地址上鎖atomic.go:11    0x4b1b2a        48ff442418              add qword ptr [rsp+0x18], 0x5 #a+=5操作,add匯編指令完成atomic.go:13    0x4b1b2f        440f117c2448            movups xmmword ptr [rsp+0x48], xmm15atomic.go:13    0x4b1b35        440f117c2458            movups xmmword ptr [rsp+0x58], xmm15atomic.go:13    0x4b1b3b        488d4c2448              lea rcx, ptr [rsp+0x48]atomic.go:13    0x4b1b40        48894c2428              mov qword ptr [rsp+0x28], rcxatomic.go:13    0x4b1b45        8401                    test byte ptr [rcx], alatomic.go:13    0x4b1b47        488d1592a00000          lea rdx, ptr [rip+0xa092]#...省略

二、LOCK匯編指令

x86指令之LOCK

2.1、 LOCK

在x86架構下,當指令附加LOCK前綴時,CPU會在執行該指令期間置位LOCK#信號,將指令轉換為?原子操作?。在多核環境下,LOCK#信號確保CPU在信號置位期間獨占操作任何共享內存。

LOCK前綴僅可附加于以下指令,且?目標操作數必須為內存操作數?:

ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, CMPXCHG16B, DEC, INC,
NEG, NOT, OR, SBB, SUB, XOR, XADD, XCHG。

2.2、 原理

在單核環境下,不存在對某個內存地址出現競爭讀寫的情況,故我們只需討論多核環境。
現代CPU基本都是多核架構,并且為了平衡CPU執行指令與內存讀寫之間的效率差距,引入了三級緩存,從緩存角度看,其結構如下圖:
緩存視角CPU結構

L1 緩存距離 CPU 核心最近,具有最快的訪問速度但容量較小。通常,L1 緩存會分為兩部分:數據緩存(L1 Data)和指令緩存(L1 code)。這是因為代碼和數據的更新策略不同,需要分別進行緩存管理。
L2 緩存的容量比 L1 緩存大,但訪問速度稍慢。它充當了中間緩沖區的作用,當 L1 緩存未命中時,CPU 會嘗試從 L2 緩存中獲取數據。每個核心擁有獨立的 L2 緩存。
L3 緩存通常具有更大的容量,但訪問速度相對較慢。它在多核心處理器中扮演著重要的角色,多個核心共享 L3 緩存中的數據。如果 L1 和 L2 緩存都未命中,CPU 核心會嘗試從 L3 緩存中獲取數據,以減少直接訪問主內存的高延遲。
主內存的容量更大,但訪問速度相對更慢。

2.2.1、 緩存行

由于每一級緩存的訪問都是有成本的,所以本級緩存向下一級取數據時的基本單位并不是字節,而是緩存行(cache line)。每個cache line包含Flag、Tag和Data,通常Data的大小是64字節,但不同型號 CPU 的 Flag 和 Tag 可能不相同。數據是按照緩存塊大小從內存向緩存加載和從緩存寫回內存的,也就是說,即使緩存只訪問1字節的數據,也得把這個字節附近以緩存行對齊的64字節的數據加載到緩存中。
CPU緩存行

  • FLAG:用于指示緩存行的狀態,例如是否有效、是否被修改等;
  • TAG:用于記錄該緩存行所對應的內存地址,這樣同一緩存塊在不同核心的緩存中,也能識別出來;
  • DATA:實際存儲的數據,通常是64字節。

緩存行大小一般是64字節,從下級緩存取數據一定是64字節,假設內存地址[0,1024],那么CPU讀取第89地址內容時,會直接取[64,128]這整個內存塊。取內存塊時從0開始以64字節為單位進行對齊的。

緩存行(cache line)緩存管理的最小存儲單元,也被稱為緩存塊.

2.2.2、 緩存一致性之MESI協議

多級緩存在提高CPU性能的同時,由于不是所有核共有的,就有了緩存一致性的問題,因此誕生了MESI協議(一致性協議除了MESI,還有其改進版MOESI、簡化版MSI、其他如Dragon?協議)。

由?伊利諾伊大學(或稱伊利諾斯州立大學)?的研究者提出,因此也稱為?伊利諾斯協議(Illinois Protocol)。MESI協議是早期?緩存一致性協議?的代表,其設計背景與?多核處理器?的發展密切相關,尤其是在需要解決共享數據一致性問題的場景中。協議名稱來源于其核心四種狀態:?Modified(M)?、?Exclusive(E)?、?Shared(S)?、?Invalid(I)。

1:狀態機

  • 已修改(Modified,簡稱M):表示緩存塊中的數據與內存中的數據不一致,即緩存塊是臟的。如果其他核心要讀取這塊數據,那么必須先將緩存塊的數據寫回內存,然后將緩存塊的狀態變為共享(S)。在這種狀態下,確保其他核心可以讀取到最新的內存數據,并維護一致性。
  • 獨占(Exclusive,簡稱E):表示緩存塊只存在于當前核心的緩存中,并且是干凈的,即與內存中的數據一致。如果其他核心要讀取這個緩存塊,那么當前核心會將自己的緩存塊的狀態變為共享(S)。當當前核心寫入數據時,緩存塊的狀態變為已修改(M)。
  • 共享(Shared,簡稱S)表示緩存塊是共享的,存在于當前核心和其他核心的緩存中,并且是干凈的,即與內存中的數據一致。在共享狀態下,多個核心可以同時讀取這個緩存塊,而且這個緩存塊隨時都可以被其他緩存塊替換,并且不需要寫回內存,因為它的內容與內存中的數據一致。
  • 無效Invalid (I):緩存行是無效的。
    該狀態信息存在緩存行結構的FLAG中.
    2:事件

為了解決多個核心之間的數據傳播問題,Intel公司提出了總線嗅探(Bus Snooping)策略,即把讀寫請求事件都通過總線廣播給所有核心,各個核心能夠嗅探到這些請求,然后根據本地緩存塊狀態對其進行響應。

1)緩存塊收到來自核心的事件

  • PrRd事件指的是讀取事件,其中Pr代表Processor(處理器)。該事件表示某個核心對一個緩存塊發起了一個讀取操作,希望獲取對這個緩存塊的共享訪問權限;
  • PrWr事件指的是寫入事件,其中Pr也代表Processor。該事件表示某個核心對一個緩存塊發起了一個寫入操作,希望獲取對這個緩存塊的獨占訪問權限。
    2)緩存塊收到來自總線的事件
  • BusRd事件表示某個核心發起了一個讀操作,希望獲取對某個緩存塊的共享訪問權。該事件使得多個核心能夠在需要時同時共享相同的數據,從而提高系統性能并有效管理共享數據。不涉及對緩存塊的修改,因此不需要執行失效或寫回操作,確保了共享數據的正確同步;
  • BusRdX事件表示某個核心發起了一個讀-修改-寫的操作,希望獲取對特定緩存塊的獨占訪問權限。這個事件的發生意味著該核心需要讀取該緩存塊的數據并執行修改,可能導致其他核心的緩存失效或需要寫回內存,以確保在修改之前獲取最新的數據狀態;
  • BusUpgr表示某個核心發起了升級請求,希望從對某個緩存塊的共享訪問切換到獨占訪問。這個事件觸發時,核心通常需要將其他核心的相同緩存塊的狀態切換為無效或將其寫回內存,以確保在執行修改操作之前,該核心獲得最新且獨占性的數據狀態;
  • Flush事件表示某個核心對一個緩存塊進行刷新操作。這可能包括將緩存塊的數據寫回內存或者通知其他核心將其緩存中的對應數據失效。通過執行 Flush 操作,系統確保所有核心使用相同的、最新的數據,避免緩存中的數據與內存不一致;
  • FlushOpt事件也表示某個核心對一個緩存塊執行刷新操作,但不是寫回內存,而是將整個緩存塊發送給另一個核心,即緩存到緩存的傳遞。

------------------------------------------------- MESI協議緩存塊在不用狀態下收到不同事件的行為
MESI不用狀態的下各事件行為
ps:其實緩存一致性,除了多核、多級緩存外,DMA也會引起緩存一致性問題。

在x86發展過程中,不是一開始就是多級緩存的:
1:一開始就是單核直接訪問內存;
2:后因為性能問題,即cpu執行指令與訪問內存速度上存在巨大差異,引入了多級緩存;
3:后來單核性能也不足,就提出了多核機制,也就引入了MESI等協議(只作用于處理器內部,與DMA無關),形成了現代CPU的普遍架構。

2.2.3、lock原理

早期緩存一致性的解決方案比較粗暴,某個cpu核心LOCK指令發出后會直接鎖總線(獨占總線),這樣其他cpu核心或MDA根本無法通過總線總內存讀取數據了,也就不存在緩存一致性問題了。

弊端顯而易見,鎖范圍過大,在引入多級緩存時,為了更好的管理內存,提出了緩存行,基于此,LOCK指令不在鎖總線,而是只鎖操作值所在的緩存行。

假如兩個CPU核心都持有相同的緩存行,且各自狀態為S,此時core0 執行lock指令要修改緩存行內某個值,會出現如下流程:

  1. 總線通過總線嗅探(Bus Snooping)策略檢測到沖突,向core1 發出BusRdX事件;
  2. core1 收到BusRdX事件,將core1本地對應緩存行狀態置為I;
  3. core0 執行操作,將core0本地對應緩存行狀態置為M,并更改相應操作值;
  4. 假如接著core1 也要執行lock指令修改該操作值,總線通過總線嗅探(Bus Snooping)策略檢測到沖突,向core0 發出BusRdX事件;
  5. core0 收到BusRdX事件,將core0本地對應緩存行狀態置為I,并通過FlushOpt事件將緩存行發給core1;
  6. core1收到core0發來的緩存行后,將core1本地對應緩存行狀態置為M,并更改相應操作值;

三、x86緩存發展

  1. 單核時代(1978–2000 初期)??

早期的 x86 CPU(如 ??8086、80286、80386??)是純粹的單核設計,??沒有緩存??:

無緩存階段??:CPU 直接訪問內存,性能受限于內存速度。
??緩存引入??:
??80486(1989)??:首次在芯片內集成 ??L1 緩存??(8 KB),顯著減少內存訪問延遲。
??Pentium(1993)??:分離為 ??指令 L1 緩存?? 和 ??數據 L1 緩存??(各 8 KB),提升并行性。
??Pentium Pro(1995)??:首次引入 ??L2 緩存??(256 KB–1 MB),但位于 CPU 封裝外的獨立芯片(速度較慢)。
??Pentium III(1999)??:將 L2 緩存集成到 CPU 芯片內(256 KB),速度大幅提升。

??2. 多級緩存的完善(2000 初期)??

隨著單核性能提升遇到瓶頸,緩存層級逐漸擴展:

??L3 緩存的引入??:
??Intel Xeon(2003)??:為服務器市場引入 ??L3 緩存??(共享緩存),優化多任務性能。
??Core 2 系列(2006)??:在消費級 CPU 中普及 L2/L3 緩存(如 4 MB L2 緩存)。
??緩存邏輯的優化??:引入緩存一致性協議(如 ??MESI??)、非阻塞緩存、預取技術等。

??3. 多核技術的誕生(2005 年后)??

單核頻率提升遭遇物理極限(功耗、散熱),轉向多核并行:

??早期嘗試??:
??Pentium D(2005)??:通過“雙芯封裝”(兩個獨立單核)實現“偽多核”,無共享緩存。
??Core 2 Duo(2006)??:真正的 ??原生雙核設計??,共享 L2 緩存,標志多核時代開啟。
??多核與緩存的協同??:
??每個核心獨立 L1/L2 緩存??:減少核心間競爭。
??共享 L3 緩存??(如 Intel Nehalem, 2008):提升多核數據共享效率。
??NUMA 架構??:在服務器 CPU 中優化多核內存訪問。

四、x86 DMA發展

DMA(直接內存訪問)控制器繞過 CPU,直接讀寫內存。

如果 CPU 緩存未與內存同步,會導致數據不一致,解決:
1)總線仲裁,當 CPU 和 DMA 同時請求訪問內存時,總線仲裁器會按仲裁策略選擇優先分給誰,另一個等待。
2)CPU緩存刷新,當 CPU緩存與主存不一致時,DMA 請求訪問內存時總線會先讓CPU將對應緩存寫回主存;
3)DMA將數據寫回內存時,強制 CPU 丟棄其緩存中的舊數據(即對應緩存行狀態置為I)

事件關鍵產品/技術意義
1976年?Intel 8257 DMA控制器發布首款商用可編程DMA控制器,奠定基礎架構
?1980年?8237芯片組集成DMA控制器DMA技術普及至PC主板
?1993年?奔騰系列芯片組支持UDMA優化存儲設備性能
??2023年?酷睿Ultra集成雷電5與DMA加速融合高速I/O與DMA技術,提升擴展性

參考

1]:十年碼農內功:緩存篇(第2版)
2]:Linux CPU 多級緩存與實踐

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

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

相關文章

mapreduce打包運行

maven打包 MapReduce是一個分布式運算程序的編程框架,是用戶開發“基于Hadoop的數據分析應用”的核心框架。 MapReduce核心功能是將用戶編寫的業務邏輯代碼和自帶默認組件整合成一個完整的分布式運算程序(例如:jar包)&#xff0…

小白成長之路-LInux系統文件與目錄管理(二)

提示:第二部分對第一部分收尾 文章目錄 常見的命令如下一、文件查看命令1. more命令2.less命令3.head命令4.tail命令5.nl命令(了解)6.創建目錄命令7.創建文件命令>: 覆蓋重定向>>: 追加重定向 8.touch命令9.echo命令10.文件或目錄復…

JVM之虛擬機運行

虛擬機運行快速復習 try-catch:catch-異常表棧展開,finally-代碼復制異常表兜底 類的生命周期:加載,連接(驗證,準備,解析),初始化,使用,卸載 類…

AI數字人實現原理

隨著人工智能與數字技術的快速發展,AI數字人(Digital Human)作為新一代人機交互媒介,正在多個行業中快速落地。無論是在虛擬主播、在線客服、教育培訓,還是在數字代言、元宇宙中,AI數字人都扮演著越來越重要…

Android開發-數據庫SQLite

在Android應用開發中,當需要存儲結構化數據時,SQLite是一個非常強大的工具。SQLite是一款輕量級的關系型數據庫管理系統,它內嵌于Android系統中,支持SQL語法,并且不需要單獨的服務器進程或系統配置。本文將介紹如何在A…

android實現USB通訊

在 Android 上枚舉 USB 設備除了使用 UsbManager.getDeviceList() 方法外,還有以下幾種常見的方式: 1. 使用 USB 設備過濾器(XML 配置) 通過在 AndroidManifest.xml 中配置 USB 設備過濾器,可以讓系統自動檢測并通知…

FFmpeg視頻編碼的完整操作指南

步驟如下: 安裝和準備FFmpeg:確保包含所需編碼器(如libx264)。基本命令行編碼:使用ffmpeg命令進行轉碼,設置視頻編碼器、CRF、預設等。API編碼流程(針對開發者): a. 注冊…

鴻蒙 UIAbility組件與UI的數據同步和窗口關閉

使用 EventHub 進行數據通信 Stage模型概念圖 根據 Stage 模型概念圖 UIAbility 先于 ArkUI Page 創建 所以,事件要先 .on 訂閱 再 emit 發布 假如現在有頁面 Page1 和他的 UIAbility // src/main/ets/page1ability/Page1Ability.ets onCreate(want: Want, laun…

全棧工程師實戰手冊:LuatOS日志系統開發指南!

本文聚焦LuatOS-log庫的實戰應用場景,通過完整案例演示日志模塊集成、格式定制及遠程同步方案,幫助全棧開發者構建靈活可靠的日志管理框架。下面,我們一起來認識LuatOS的log庫! 一、 log.info() log info()主要打印一些正常的…

STM32-USART串口通信(9)

一、通信接口介紹 通信的目的:將一個設備的數據傳送到另一個設備,擴展硬件系統。 當STM32想要實現一些功能,但是需要外掛一些其他模塊才能實現,這就需要在兩個設備之間連接上一根或多跟通信線,通過通信線路發送或者接…

【MoveIt 2】使用 MoveIt 任務構造器(MoveIt Task Constructor)進行拾取和放置

本教程將引導您創建一個使用 MoveIt 任務構造器規劃抓取和放置操作的包。MoveIt 任務構造器(https://github.com/moveit/moveit_task_constructor/tree/ros2/)提供了一種為包含多個不同子任務(稱為階段)的任務進行規劃的方法。如果…

破解商業綜合體清潔管理困局:商業空間AI智能保潔管理系統全場景解決方案

方案整體概述 隨著商業綜合體日益向智能化、精細化管理轉型,傳統保潔工作面臨人員監管難、清潔效果評估難、應急響應滯后等諸多挑戰。為解決這些痛點,本系統依托計算機視覺、行為識別、圖像分割與深度學習等AI技術,構建一套集人員管理、工作…

spring響應式編程系列:異步消費數據

目錄 示例 大致流程 parallel cache PARALLEL_SUPPLIER newParallel init publishOn new MonoSubscribeOnValue ???????subscribe ???????new LambdaMonoSubscriber ???????MonoSubscribeOnValue.subscribe ???????onSubscribe ??…

視頻編解碼學習十二之Android疑點

一、android.view.SurfaceControl.setDisplaySurface的作用 android.view.SurfaceControl.setDisplaySurface 是 Android 系統中一個 native 層級別的 API,主要用于 設置某個物理顯示屏(Display)的輸出 Surface,屬于 SurfaceFlin…

家用或辦公 Windows 電腦玩人工智能開源項目配備核顯的必要性(含 NPU 及顯卡類型補充)

一、GPU 與顯卡的概念澄清 首先需要明確一個容易誤解的概念:GPU 不等同于顯卡。 顯卡和GPU是兩個不同的概念。 【概念區分】 在討論圖形計算領域時,需首先澄清一個常見誤區:GPU(圖形處理單元)與顯卡(視…

Python----神經網絡(《Deep Residual Learning for Image Recognition》論文和ResNet網絡結構)

一、論文 1.1、論文基本信息 標題:Deep Residual Learning for Image Recognition 作者:Kaiming He, Xiangyu Zhang, Shaoqing Ren, Jian Sun 單位:Microsoft Research 會議:CVPR 2016 主要貢獻:提出了一種深度殘…

Qt/C++開發監控GB28181系統/錄像文件查詢/錄像回放/倍速播放/錄像文件下載

一、前言 搞定了實時預覽后,另一個功能就是錄像回放,錄像回放和視頻點播功能完全一致,唯一的區別就是發送點播的sdp信息中攜帶了開始時間和結束時間,因為是錄像文件,所以有這個時間,而實時視頻預覽這個對應…

在Spark搭建YARN

(一)什么是SparkONYarn模式 Spark on YARN(Yet Another Resource Negotiator)是 Spark 框架在 Hadoop 集群中運行的一種部署模式,它借助 Hadoop YARN 來管理資源和調度任務。 架構組成 ResourceManager:作…

SpringAI

機器學習: 定義:人工智能的子領域,通過數據驅動的方法讓計算機學習規律,進行預測或決策。 核心方法: 監督學習(如線性回歸、SVM)。 無監督學習(如聚類、降維)。 強化學…

如何用Redis實現分布式鎖?RedLock算法的核心思想?Redisson的看門狗機制原理?

一、Redis分布式鎖基礎實現 public class RedisDistributedLock {private JedisPool jedisPool;private String lockKey;private String clientId;private int expireTime 30; // 默認30秒public boolean tryLock() {try (Jedis jedis jedisPool.getResource()) {// NX表示不…