色哟哟视频在线观看-色哟哟视频在线-色哟哟欧美15最新在线-色哟哟免费在线观看-国产l精品国产亚洲区在线观看-国产l精品国产亚洲区久久

0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
电子发烧友
开通电子发烧友VIP会员 尊享10大特权
海量资料免费下载
精品直播免费看
优质内容免费畅学
课程9折专享价
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

Ringbuffer的性能優(yōu)化方法

安芯教育科技 ? 來源:極術(shù)社區(qū) ? 2025-03-24 16:03 ? 次閱讀

本文轉(zhuǎn)載于極術(shù)社區(qū)

作者:非典型程序員

Ringbuffer(循環(huán)緩存)是軟件中非常常用的數(shù)據(jù)結(jié)構(gòu)之一, 在互聯(lián)網(wǎng)應(yīng)用、數(shù)據(jù)庫應(yīng)用等中使用廣泛。處理器執(zhí)行 Ringbuffer 的效率與其存儲系統(tǒng)處理共享數(shù)據(jù)的性能息息相關(guān)。

本文旨在介紹 Ringbuffer 的性能優(yōu)化方法。本文介紹基于 mutex 的 Ringbuffer 實(shí)現(xiàn)、阻塞的無鎖實(shí)現(xiàn)以及非阻塞的無鎖實(shí)現(xiàn),并且比較和分析這些不同的實(shí)現(xiàn)在 Arm 服務(wù)器平臺上的性能。

聲明:本文介紹的針對 Ringbuffer 實(shí)現(xiàn)的優(yōu)化方法來自于 Ola Liljedahl 的研究。Ola 現(xiàn)任 Arm 公司的架構(gòu)師,長期專注于網(wǎng)絡(luò)軟件的性能優(yōu)化設(shè)計(jì),尤其是可擴(kuò)展的多線程共享存儲編程。針對 Arm A 系列處理的多線程性能,他開發(fā)了一個可擴(kuò)展的多線程并行庫progress64(github.com/ARM-software/progress64)。

本文分析了 Ola 提出的主要優(yōu)化手段,并且在 Arm服務(wù)器上重現(xiàn)了 Ola 的優(yōu)化以驗(yàn)證其實(shí)際效果。博客中使用的代碼只是示例代碼(https://github.com/wangeddie67/ringbuffer_opt_demo),其功能完備性和魯棒性都不能滿足實(shí)際應(yīng)用的需求。

介紹

Ringbuffer 通常由一個包含數(shù)據(jù)的數(shù)組和指向隊(duì)列頭(Head)和隊(duì)尾(Tail)的指針構(gòu)成(如圖 1 左邊所示)。指針只能按照一個方向移動。當(dāng)指針移動到數(shù)組結(jié)尾時,指針會折回到隊(duì)列開始。

Head 指針表示下一個可以彈出的數(shù)據(jù);當(dāng)數(shù)據(jù)彈出隊(duì)列(稱為 Dequeue)時,Head 指針加 1。Tail 指針表示下一個可以壓入數(shù)據(jù)的單元;當(dāng)數(shù)據(jù)壓入隊(duì)列(稱為 Enqueue)時,Tail 指針加 1;

從邏輯上看,Ringbuffer 中的數(shù)組形成了一個環(huán)(如圖 1 右邊所示)。頭和尾指針在這個環(huán)上沿著相同的方向移動。這就是 Ringbuffer 名稱的由來。

wKgZPGfhEe6AD6ChAAD--8CP8V8610.png

圖 1 Ringbuffer 示意圖

向 Ringbuffer 壓入單元的程序稱為生產(chǎn)者(Producer),從 Ringbuffer 彈出單元的程序稱為消費(fèi)者(Consumer),如圖 2 所示。一個 Ringbuffer 通常具有很多的生產(chǎn)者和消費(fèi)者,每個生產(chǎn)者和消費(fèi)者都是不同的線程或進(jìn)程。這些線程或進(jìn)程會被調(diào)度到系統(tǒng)中的不同處理器核心上,而 Ringbuffer 則是不同處理器核心之間共享的數(shù)據(jù)。

wKgZO2fhEe6AWyPKAACXnd50NAs828.png

圖 2 生產(chǎn)者和消費(fèi)者

基于 Mutex 的 Ringbuffer 實(shí)現(xiàn)

為了保證共享數(shù)據(jù)的功能正確,通常使用 mutex 來進(jìn)行保護(hù) Ringbuffer,以保證只有一個生產(chǎn)者或消費(fèi)者能夠訪問或操作 Ringbuffer。

基于 Mutex 的 Ringbuffer 聲明

Ringbuffer 的數(shù)據(jù)結(jié)構(gòu)如代碼塊 1 所示。

// Entry in ring buffer
class BufferEntry
{
public:
    void *mp_ptr;    // Pointer to data entry.
};


class RingBuffer
{
public:
    int m_mutex;                // Mutex for shared pointers.
    unsigned int m_size;        // The number of physical entries.


    unsigned int m_head;        // Head pointer.
    unsigned int m_tail;        // Tail pointer.


    BufferEntry *mp_entries;    // Pointer to physical entries.
};

代碼 1 基于 Mutex 的 Ringbuffer 聲明

數(shù)據(jù)數(shù)組中每個單元BufferEntry只提供一個指針,指針指向?qū)嶋H需要進(jìn)入 Ringbuffer 的數(shù)據(jù)結(jié)構(gòu)。當(dāng) Enqueue 和 Dequeue 時,只需要復(fù)制數(shù)據(jù)結(jié)構(gòu)的指針即可,而不需要復(fù)制數(shù)據(jù),從而減少數(shù)據(jù)拷貝的開銷。

基于 Mutex 的 Ringbuffer 的 Enqueue 和 Dequeue 函數(shù)

圖 3 展示了 Enqueue 和 Dequeue 的流程圖。圖 3 左邊展示的是阻塞操作的流程圖;右邊展示的是非阻塞操作的流程圖。如果實(shí)現(xiàn)的是阻塞式的操作,那么函數(shù)一直重復(fù)檢查空滿條件,直到 Ringbuffer 的滿足操作所需的空滿條件;如果實(shí)現(xiàn)的是非阻塞式的操作,那么函數(shù)可以在檢查空滿條件失敗后直接退出。

wKgZPGfhEfyAaYaqAAERyQdb9xE448.png

圖 3 基于 Mutex 的 Ringbuffer 實(shí)現(xiàn)流程圖

循環(huán)隊(duì)列的空滿控制有很多種方法。在本系列文章的示例中采用邏輯單元和物理單元的控制方法。head 和 tail 的范圍并不只限于物理單元的數(shù)量,而是可以一直累加。訪問單元時,將(head MOD size)或(tail MOD size)得到對應(yīng)的物理單元。

對于 Enqueue 操作,需要 Ringbuffer 不為滿,即m_head+m_size!=m_tail(head 和 tail 之差不大于物理單元的數(shù)量)。

對于 Dequeue 操作,需要Ringbuufer 不為空,即m_head!=m_tail (head 和 tail 沒有指向同一個單元)。

基于 Mutex 的 Ringbuffer 實(shí)現(xiàn)如下:

// Blocking Enqueue Function
int enqueue_ringbuf(RingBuffer *ring_buffer, void *entry)
{
    // Lock mutex.
    lock(&ring_buffer->m_mutex);


    // Ringbuffer is full. Do nothing.
    while (ring_buffer->m_head + ring_buffer->m_size == ring_buffer->m_tail);


    // Enqueue entry.
    int enq_ptr = ring_buffer->m_tail % ring_buffer->m_size;
    ring_buffer->mp_entries[enq_ptr].mp_ptr = entry;


    // Increase tail pointer.
    ring_buffer->m_tail = ring_buffer->m_tail + 1;


    // Unlock mutex.
    unlock(&ring_buffer->m_mutex);


    return 0;
}


// Blocking Dequeue Function
int dequeue_ringbuf(RingBuffer *ring_buffer, void **entry)
{
    // Lock mutex.
    lock(&ring_buffer->m_mutex);


    // Ringbuffer is empty. Do nothing.
    while (ring_buffer->m_head == ring_buffer->m_tail);


    // Dequeue entry.
    int deq_ptr = ring_buffer->m_head % ring_buffer->m_size;
    *entry = ring_buffer->mp_entries[deq_ptr].mp_ptr;
    ring_buffer->mp_entries[deq_ptr].mp_ptr = NULL;


    // Increase head pointer.
    ring_buffer->m_head = ring_buffer->m_head + 1;


    // Unlock mutex.
    unlock(&ring_buffer->m_mutex);


    return 0;
}


代碼 2 基于 Mutex 的 Ringbuffer 實(shí)現(xiàn)


Lock 和 Unlock


Ringbuffer 最多允許一個生產(chǎn)者或一個消費(fèi)者對于 Ringbuffer 進(jìn)行操作。這需要通過鎖標(biāo)志m_mutex進(jìn)行控制。考慮到 pthread 中的 mutex API 引入的系統(tǒng)調(diào)度開銷過大,這里采用__sync_val_compare_and_swap原語實(shí)現(xiàn) lock 和 unlock 操作。

type __sync_val_compare_and_swap(type* ptr, type oldval, type newval);

__sync_val_compare_and_swap原語提供了同步和原子 CAS 操作兩個功能,其功能核心是 CASAL 指令。原語將ptr指向的變量與oldval比較,如果值相同則將ptr指向的變量修改為newval,并返回ptr指向變量的舊值;反之,則不修改ptr指向的變量,并返回ptr指向變量的值。同時,原語保證了 load/store 的同步,即在指令序上先于原語的 load/store 已經(jīng)完成,指令序上晚于原語的 load/store 不會執(zhí)行。
用__sync_val_compare_and_swap原語實(shí)現(xiàn)的 lock 和 unlock 功能如下:
// Lock function
void lock(int *mutex)
{
    while (__sync_val_compare_and_swap(mutex, 0, 1) != 0)
    {
    }
}


// Unlock function
void unlock(int *mutex)
{
    __sync_val_compare_and_swap(mutex, 1, 0);
}

代碼 3 lock 和 unlock 函數(shù)的實(shí)現(xiàn)

mutex變量為 0 表示mutex釋放;變量為 1 表示mutex鎖定。在 lock 函數(shù)中,如果當(dāng)前mutex為 0(釋放),則更新為 1(鎖定),并且 lock 函數(shù)返回 0;反之則 lock 函數(shù)返回 1。 基于 mutex 的 ringbuffer 實(shí)現(xiàn)的完整源碼請參見(https://github.com/wangeddie67/ringbuffer_opt_demo/blob/main/srcs/mutex_blkring.h)。

測試框架

測試程序包含一個控制測試過程的主線程和多個充當(dāng)生產(chǎn)者和消費(fèi)者的子線程。主線程首先創(chuàng)建并初始化 Ringbuffer,并且創(chuàng)建指定數(shù)量的子線程,最后等待子線程結(jié)束。子線程數(shù)量可以配置。 每個子線程即使生產(chǎn)者也是消費(fèi)者。每次測試迭代從隊(duì)列中讀取一個單元,再將這個單元(不做額外操作)插入隊(duì)尾,即一次 Dequeue 和一次 Enqueue 操作。整個測試過程需要執(zhí)行的 Enqueue 和 Dequeue 操作數(shù)量相同。如果將一部分線程設(shè)置為生產(chǎn)者,另一部分線程設(shè)置為消費(fèi)者,那么數(shù)據(jù)單元一定由生產(chǎn)者向消費(fèi)者移動。這可能在互聯(lián)結(jié)構(gòu)中出現(xiàn)具有方向性的數(shù)據(jù)流,從而加強(qiáng)測試結(jié)果與線程調(diào)度的核心的物理位置的關(guān)系。 為了保證子線程同步啟動測試,子線程首先會陷入 barrier。當(dāng)所有子程序都創(chuàng)建完成后,子線程從 barrier 退出。每個線程執(zhí)行的測試迭代數(shù)量都相同。

f2578c98-05e9-11f0-9310-92fbcf53809c.png

圖 4 測試程序流程圖 每個子線程獨(dú)立統(tǒng)計(jì)測試階段完成的時間,并且分別計(jì)算操作的吞吐率。這是為了避免統(tǒng)計(jì)過程引入新的共享數(shù)據(jù)。最后,主線程獲取每個子線程統(tǒng)計(jì)的操作吞吐率,并且求和得到總吞吐率。吞吐率用每秒執(zhí)行的 Enqueue 或 Dequeue 操作數(shù)量表示,即 op/s。 由于 Ringbuffer 的性能與存儲系統(tǒng)的性能有直接關(guān)聯(lián),因此子線程調(diào)度到的核心對于具體實(shí)現(xiàn)具有很大的關(guān)系。為了保證測試的穩(wěn)定性,需要將子線程調(diào)度到指定的核心(綁核)。如果不做綁核,那么相同實(shí)現(xiàn)的相同配置的不同測試結(jié)果可能會大相徑庭,測試結(jié)果偏差極大。 測試框架的源碼請參見https://github.com/wangeddie67/ringbuffer_opt_demo/blob/main/testbench/testbench.cc)。

基于 Mutex 的 Ringbuffer 實(shí)現(xiàn)的測試結(jié)果

測試采用的 Arm 服務(wù)器具有 32 個 N1 處理器,使用 CMN600 作為互聯(lián)結(jié)果。測試用的 Arm 服務(wù)器上并沒有區(qū)分 NUMA 結(jié)點(diǎn),因此采用自然映射的關(guān)系,即子線程 0 調(diào)度到 CPU0;子線程 1 調(diào)度到 CPU1,以此類推。 圖 5 展示了基于 Mutex 的 Ringbuffer 的性能測試結(jié)果。顯然,隨著子線程數(shù)量的增加,Ringbuffer 的吞吐率顯著降低。當(dāng)使用 2 個子線程時,吞吐率為 3985230 op/s;當(dāng)使用全部 32 個子線程時,吞吐率為 323111 op/s。前者是后者的 12.3 倍。

f25b6a02-05e9-11f0-9310-92fbcf53809c.png

圖 5 基于 Mutex 的 Ringbuffer 性能測試

圖 5 展示的性能測試結(jié)果是典型的以多線程競爭為主導(dǎo)的性能曲線。隨著參與競爭的子線程數(shù)增加,在競爭 mutex 上花費(fèi)的時間就更多,導(dǎo)致性能快速下降。 使用 perf 工具對于啟動 16 個線程的測試程序進(jìn)行熱點(diǎn)分析,熱點(diǎn)分析結(jié)果如圖 6 所示。圖中橙色部分提示了lock函數(shù)占用了程序執(zhí)行時間的 91%。因此,對于 Ringbuffer 進(jìn)行優(yōu)化的首要步驟就是采用無鎖的實(shí)現(xiàn)方式。 f26893b2-05e9-11f0-9310-92fbcf53809c.png 圖 6 Perf 熱點(diǎn)分析結(jié)果

無鎖 Ringbuffer 實(shí)現(xiàn)

對于多線程共享程序,需要使用 mutex 來保護(hù)對于共享變量的 load-modify-store 代碼序列,從而保證功能正確性。但是 mutex 的競爭引入了非常明顯的性能開銷。因此,需要探索無鎖的 Ringbuffer 實(shí)現(xiàn)。

用原子操作替代鎖

除了 mutex,另一種可以保護(hù)共享變量的方法是原子操作。單個原子操作就是由 load-modify-store 三個部分構(gòu)成。原子操作保證,在完成 load-modify-store 序列之前,被訪問的地址不會被其他指令訪問。典型的原子操作包括 Compare-and-swap (CAS),以及 Arm 提供的 LDADD 等原子操作。如果有多個 PE 針對同一個共享變量發(fā)起多個原子操作,那么這些操作只能按照某種順序串行,而不能并行。執(zhí)行順序取決于硬件實(shí)現(xiàn)。 CAS 保證了對于 mutex 的操作都是串行的。例如 PE1 執(zhí)行原子操作 CAS(x, 0, 1),同時 PE2 執(zhí)行原子操作 CAS(x, 0, 2):

如果 PE1 先讀取到 x 的值,那么 PE1 的 CAS 操作成功;PE2 讀取到的 x 的值是 PE1 更新后的值=1,CAS 操作失敗。最后 PE1 中返回值是 0;PE2 中返回值是 1;x 的值是 1。

如果 PE2 先讀取到 x 的值,那么 PE2 的 CAS 操作成功;PE1 讀取到的 x 的值是 PE2 更新后的值=2,CAS 操作失敗。最后 PE1 的返回值是 2;PE2 的返回值是 0;x 的值是 2。

在 Ringbuffer 實(shí)現(xiàn)中,特別需要保護(hù)的就是被所有生產(chǎn)者和消費(fèi)者共享的 head 和 tail 指針。可以用__atomic_fetch_add原語來實(shí)現(xiàn)指針的原子指令并且實(shí)現(xiàn)指針的累加。 __atomic_fetch_add原語的聲明如下,其核心指令是 LDADD。原語讀取ptr指向的變量,然后將變量累加val后的新值會寫到原來的位置;同時,原語將ptr指令的變量的舊值返回。

type __atomic_fetch_add (type *ptr, type val, int memorder)
將__atomic_fetch_add原語應(yīng)用到 head 或 tail 指針上,則可以將存儲在內(nèi)存中的指針變量加 1,并且同時將加 1 前的舊值返回。這就相當(dāng)于將目前 head/tail 指針指向的單元分配給了當(dāng)前的 Enqueue 或 Dequeue 的生產(chǎn)者或消費(fèi)者,同時與其他生產(chǎn)者或消費(fèi)者同步了更新后的指針。

Enqueue 和 Dequeue

圖 7 展示了 Enqueue 和 Dequeue 的流程圖。

f279e6da-05e9-11f0-9310-92fbcf53809c.png

圖 7 無鎖 Ringbuffer 實(shí)現(xiàn)流程圖(使用原子操作替換 mutex) 相對于基于 mutex 的 Ringbuffer 實(shí)現(xiàn),無鎖 Ringbuffer 實(shí)現(xiàn)從軟件和硬件兩方面的優(yōu)化。 從軟件的角度,無鎖 Ringbuffer 的關(guān)鍵區(qū)要小很多。在基于 mutex 的 Ringbuffer 實(shí)現(xiàn)(圖 8 左側(cè))中,鎖保護(hù)了整個 Enqueue 和 Dequeue 過程,每個 Enqueue 和 Dequeue 過程都只能串行執(zhí)行。在無鎖 Ringbuffer 實(shí)現(xiàn)(圖 8 右側(cè))中,原子指令只保護(hù)了對于 head 或 tail 指針的累加。除了__atomic_fetch_add必須串行,其他部分都是可以并行的,從而增加了程序的并行度。

f2851bc2-05e9-11f0-9310-92fbcf53809c.png

圖 8 基于 mutex 的 Ringbuffer 實(shí)現(xiàn)和無鎖 Ringbuffer 實(shí)現(xiàn)的關(guān)鍵區(qū)(著色部分)對比 從硬件的角度,原子指令引入更少的 snoop 操作。使用常規(guī)的 load 指令讀取數(shù)據(jù)后,在 PE 側(cè)的緩存中的數(shù)據(jù)副本的狀態(tài)為 Share(共享)。所有訪問這個數(shù)據(jù)的 PE 的緩存都會具有這個共享數(shù)據(jù)的副本。當(dāng)共享數(shù)據(jù)被某個 PE 修改后,需要大量的 snoop 去失效其他 PE 中的所有副本。 使用原子指令讀取數(shù)據(jù)之后,數(shù)據(jù)的緩存狀態(tài)為 Exclusive(獨(dú)有),即只有一個 PE 具有這個共享數(shù)據(jù)的副本。當(dāng)另一個 PE 也執(zhí)行原子操作后,只需要一個 snoop 就可以將已經(jīng)存在副本清除。 從帶寬的角度,更少 snoop 數(shù)量意味著 snoop 通信占用的通信帶寬更少,提供給數(shù)據(jù)通信的帶寬就會更高。從延遲的角度,load/store 操作需要等到其引起的所有 snoop 都完成才能被標(biāo)識為完成。更少的 snoop 數(shù)量意味著等待 snoop 完成的時間更短。 無鎖 Ringbuffer 同時從這兩方面優(yōu)化中受惠,暫時沒有辦法定量分析出哪一個優(yōu)化的作用更大。一般來說,縮小關(guān)鍵區(qū)的作用更大。

邏輯單元序號

上述的無鎖 Ringbuffer 仍然可能導(dǎo)致功能錯誤。例如一個 Ringbuffer 有 16 個物理單元。首先為生產(chǎn)者 A 分配的邏輯單元是 16;隨后,為生產(chǎn)者 B 分配的單元是 32;這兩個單元映射到相同的物理單元 0。雖然單元分配和指針更新是串行的,但是對于數(shù)據(jù)單元的訪問是并行的,因此并不能保證生產(chǎn)者 B 訪問物理單元 0 時,生產(chǎn)者 A 一定完成了操作。可能生產(chǎn)者 A 和生產(chǎn)者 B 同時查詢到單元是空閑,進(jìn)而同時對這個物理單元進(jìn)行寫入。此時無法保證寫入單元的究竟是生產(chǎn)者 A 的數(shù)據(jù)還是生產(chǎn)者 B 的數(shù)據(jù)。 為了解決這樣的問題,Ola 提出,給每個單元增加一個域來表示其對應(yīng)的邏輯單元序號。只有當(dāng)邏輯單元序號與 head/tail 指針一致的時候才能進(jìn)行訪問。當(dāng) Dequeue 單元的時候,需要更新邏輯單元序號,將單元的序列號增加物理單元的總數(shù)。 還是上面的例子,當(dāng)生產(chǎn)者 A 和 B 同時查詢到內(nèi)存是空閑時,由于單元的序列號與生產(chǎn)者 A 分配的單元匹配,那么生產(chǎn)者 A 可以操作這個單元;生產(chǎn)者 B 則因?yàn)樾蛄刑柌黄ヅ涠荒茉L問,需要繼續(xù)等待。 增加邏輯單元序號后的 Enqueue 和 Dequeue 流程圖如圖 9。

f28c9136-05e9-11f0-9310-92fbcf53809c.png

圖 9 無鎖 Ringbuffer 實(shí)現(xiàn)流程圖(使用原子操作替換 mutex)

無鎖 Ringbuffer 聲明

無鎖 Ringbuffer 的聲明如下。Ringbuffer 不需要 mutex 進(jìn)行保護(hù)。

// Entry in ring buffer
class BufferEntry
{
public:
    unsigned int m_sn;  // Sequential number.
    void *mp_ptr;       // Pointer to data entry.
};


class RingBuffer
{
public:
    unsigned int m_size;        // The number of physical entries.
 
    unsigned int m_head;      // Head pointer.
    unsigned int m_tail;      // Tail pointer.
 
    BufferEntry *mp_entries;  // Pointer to physical entries.
}

代碼 3 無鎖 Ringbuffer 聲明

無鎖 Ringbuffer 的 Enqueue 和 Dequeue 函數(shù)

Enqueue 和 Dequeue 函數(shù)的實(shí)現(xiàn)如下。

// Blocking Enqueue Function
int enqueue_ringbuf(RingBuffer *ring_buffer, void *entry)
{
    // Update the tail pointer.
    int sn = __atomic_fetch_add(&ring_buffer->m_tail, 1, __ATOMIC_RELAXED);
    int enq_ptr = sn % ring_buffer->m_size;
 
    // Check the data entry until empty.
    unsigned int entry_sn;
    void *entry_ptr;
    do
    {
        // Replace load by atomic load.
        // entry_sn = __atomic_load_n(&ring_buffer->mp_entries[enq_ptr].m_sn, __ATOMIC_RELAXED);
        // entry_ptr = __atomic_load_n(&ring_buffer->mp_entries[enq_ptr].mp_ptr, __ATOMIC_RELAXED);
        entry_sn = ring_buffer->mp_entries[enq_ptr].m_sn;
        entry_ptr = ring_buffer->mp_entries[enq_ptr].mp_ptr;
    } while (entry_sn != sn || entry_ptr != NULL);
 
    // Write entry.
    // Replace store by atomic store.
    // __atomic_store_n(&ring_buffer->mp_entries[enq_ptr].mp_ptr, entry, __ATOMIC_RELEASE);
    ring_buffer->mp_entries[enq_ptr].mp_ptr = entry;
 
    return 0;
}
 
// Blocking Dequeue Function
int dequeue_ringbuf(RingBuffer *ring_buffer, void **entry)
{
    // Update the head pointer.
    int sn = __atomic_fetch_add(&ring_buffer->m_head, 1, __ATOMIC_RELAXED);
    int deq_ptr = sn % ring_buffer->m_size;
 
    // Check the data entry until not empty.
    unsigned int entry_sn;
    void *entry_ptr;
    do
    {
        // Replace load by atomic load.
        // entry_sn = __atomic_load_n(&ring_buffer->mp_entries[deq_ptr].m_sn, __ATOMIC_RELAXED);
        // entry_ptr = __atomic_load_n(&ring_buffer->mp_entries[deq_ptr].mp_ptr, __ATOMIC_ACQUIRE);
        entry_sn = ring_buffer->mp_entries[deq_ptr].m_sn;
        entry_ptr = ring_buffer->mp_entries[deq_ptr].mp_ptr;
    } while (entry_ptr == NULL || entry_sn != sn);
 
    // Read entry.
    *entry = entry_ptr;
    // Replace store by atomic store
    // __atomic_store_n(&ring_buffer->mp_entries[deq_ptr].mp_ptr, 0, __ATOMIC_RELAXED);
    ring_buffer->mp_entries[deq_ptr].mp_ptr = 0;
    // Update sequence number.
    // Replace store by atomic store
    // __atomic_store_n(&ring_buffer->mp_entries[deq_ptr].m_sn, sn + ring_buffer->m_size, __ATOMIC_RELEASE);
    ring_buffer->mp_entries[deq_ptr].m_sn = sn + ring_buffer->m_size;
 
    return 0;
}

代碼 4 無鎖 Ringbuffer 的 Enqueue 和 Dequeue 函數(shù)

Enqueue 和 Dequeue 函數(shù)中對于數(shù)據(jù)單元的 load/store 操作可以進(jìn)一步被原子操作__atomic_load_n和__atomic_store_n取代。代碼 4 中的注釋給出了可以替換的原子操作。

無鎖 ringbuffer 實(shí)現(xiàn)的完整源碼請參見(https://github.com/wangeddie67/ringbuffer_opt_demo/blob/main/srcs/lockfree_blkring.h)。

使用原子操作數(shù)據(jù)單元的無鎖 Ringbuffer 實(shí)現(xiàn)的完整源碼請參見(https://github.com/wangeddie67/ringbuffer_opt_demo/blob/main/srcs/atomic_blkring.h)。

無鎖 Ringbuffer 性能測試結(jié)果

無鎖 Ringbuffer 的性能如圖 10 所示。曲線 mutex 表示基于 mutex 的 Ringbuffer 實(shí)現(xiàn);lockfree 和 atomic 表示無鎖 Ringbuffer 實(shí)現(xiàn)(lockfree 使用普通 load/store 操作數(shù)據(jù)單元;atomic 使用原子 load/store 操作數(shù)據(jù)單元)。

wKgZPGfhEiyAZ-CXAAIHqWTc3U4255.png

圖 10 無鎖 Ringbuffer 性能測試

從圖 10 中可以發(fā)現(xiàn),相對于基于 mutex 的 Ringbuffer 實(shí)現(xiàn),無鎖 Ringbuffer 實(shí)現(xiàn)的性能得到了很大的提升。用 atomic 曲線和 mutex 曲線進(jìn)行比較。當(dāng)只有 2 個線程時,無鎖 Ringbuffer 實(shí)現(xiàn)的吞吐率是基于 mutex 的 Ringbuffer 實(shí)現(xiàn)的吞吐率的 1.86 倍;當(dāng)使用 32 個線程時,無鎖 Ringbuffer 實(shí)現(xiàn)的吞吐率是基于 mutex 的 Ringbuffer 實(shí)現(xiàn)的吞吐率的 18 倍。

從圖 10 中可以發(fā)現(xiàn),對于數(shù)據(jù)單元的訪問是否使用原子操作對于性能的影響并不明顯。兩條曲線基本上是重合的。這是因?yàn)闇y試的 ringbuffer 提供了足夠的單元,多個線程共享 Ringbuffer 單元的競爭并不強(qiáng)烈。

無鎖 Ringbuffer 實(shí)現(xiàn)的吞吐率還是會隨著子線程數(shù)增加而降低,但是并不如基于 mutex 的 Ringbuffer 強(qiáng)烈。當(dāng)使用 2 個子線程時,吞吐率為 7394830 op/s;當(dāng)使用全部 32 個子線程時,吞吐率為 5800350 op/s。前者是后者的 1.27 倍。

利用 Perf 分析程序熱點(diǎn)只能定位到特點(diǎn)在 enqueue 和 dequeue 函數(shù),無法提供進(jìn)一步的信息。因此使用 Arm 的 top-down 工具對于微架構(gòu)執(zhí)行情況進(jìn)行詳細(xì)分析,得到的結(jié)果如圖 11。

wKgZO2fhEiyACdlFAAR_HdEbgXo773.png

圖 11 利用 Top-down 對無鎖 Ringbuffer 進(jìn)行性能分析

圖 11 中用紅圈標(biāo)注了主要注意的指標(biāo)。首先,程序明確屬于后端瓶頸的程序,后端阻塞周期高達(dá) 97%。其次,L1、L2 和 LC(Last-level cache)明顯高于其他異常情況,這表示程序中存在大量的數(shù)據(jù)競爭。最后 L1、L2 和 LC 的數(shù)據(jù)缺失率屬于同一量級,這表示數(shù)據(jù)競爭是全局性的,需要通過系統(tǒng)的最后一級緩存才能得到解決。top-down 的分析結(jié)果提示,無鎖的 ringbuffer 實(shí)現(xiàn)仍然導(dǎo)致了大量的全局性的數(shù)據(jù)沖突。

無鎖 Ringbuffer 的對齊改進(jìn)

通過對于程序的分析,符合上述特征的共享數(shù)據(jù)只有 head 和 tail 指針。因?yàn)?head 和 tail 指針處于相同的緩存行。這就導(dǎo)致生產(chǎn)者和消費(fèi)者仍然在競爭同一個緩存行。可以通過對與 Ringbuffer 的數(shù)據(jù)結(jié)構(gòu)進(jìn)行緩存行對齊,減少不同生產(chǎn)者和消費(fèi)者競爭同一個緩存行的情況。

Ringbuffer 實(shí)現(xiàn)的緩存行對齊包含兩個部分:

head/tail 指針的對齊。將 head 指針和 tail 指針分別放置到不同的緩存行。這可以通過 C 語言原語 alignas 實(shí)現(xiàn)。

class RingBuffer
{
public:
    alignas(64) unsigned int m_size;


    alignas(64) unsigned int m_head;
    alignas(64) unsigned int m_tail;


    alignas(64) BufferEntry *mp_entries;
}

代碼 5 對齊的無鎖 Ringbuffer 的聲明


這樣只有生產(chǎn)者競爭 tail 指針?biāo)诘木彺嫘校幌M(fèi)者競爭 head 指針?biāo)诘木彺嫘小?br />數(shù)據(jù)單元的對齊。這是為了避免訪問不同單元的線程因?yàn)閱卧幱谙嗤彺嫘卸a(chǎn)生競爭。比如訪問物理單元 0-3 的線程實(shí)際上在競爭同一個緩存行。
這一部分可以通過空間換效率的方式實(shí)現(xiàn)。一個緩存行(64B)中可以放置 4 個單元(每個單元 16B,8B 指針和 8B 序列號)。在初始化時分配 4 倍的單元,在 Enqueue 和 Dequeue 每次都只使用 4 對齊的序號。邏輯單元 0 對應(yīng)到物理單元 0;邏輯單元 1 對應(yīng)到物理單元 4;依次類推。
物理單元序號計(jì)算方法如下:

int enq_ptr = (sn % ring_buffer->m_size) * 4;

代碼 6 對齊的無鎖 Ringbuffer 映射邏輯單元到物理單元

對齊后的無鎖 ringbuffer 的內(nèi)存分布如下。其中紅色部分是實(shí)際訪問的數(shù)據(jù)單元。綠色部分是沒有使用到的數(shù)據(jù)單元。

wKgZPGfhEkiAO1v4AACy8vCdu8Y205.png

圖 12 對齊后的無鎖 Ringbuffer 的內(nèi)存分布

需要說明的是,當(dāng)物理數(shù)據(jù)單元數(shù)量遠(yuǎn)大于生產(chǎn)者和消費(fèi)者數(shù)量時,數(shù)據(jù)單元的對齊對于性能的影響非常微小。因?yàn)楣蚕硪粋€緩存行的線程數(shù)量很小,最多只有 4 個。

緩存行對齊的無鎖 ringbuffer 實(shí)現(xiàn)的完整源碼請參見(https://github.com/wangeddie67/ringbuffer_opt_demo/blob/main/srcs/align_blkring.h)。

對齊的無鎖 Ringbuffer 性能測試結(jié)果

緩存行對齊的無鎖 Ringbuffer 的性能如圖 13 所示。曲線 mutex 表示基于 mutex 的 Ringbuffer 實(shí)現(xiàn);lockfree 和 atomic 表示無鎖 Ringbuffer 實(shí)現(xiàn)(lockfree 使用普通 load/store 操作數(shù)據(jù)單元;atomic 使用原子 load/store 操作數(shù)據(jù)單元);align 表示緩存行對齊后的無鎖 Ringbuffer 實(shí)現(xiàn)。

wKgZO2fhEkiAOoOhAAH1pwNn9MU457.png

圖 13 對齊的無鎖 Ringbuffer 性能測試

緩存行對齊的無鎖 Ringbuffer 實(shí)現(xiàn)的性能曲線隨著子線程數(shù)量的增加呈現(xiàn)增長的趨勢。具體來說,

首先,當(dāng)子線程數(shù)小于 16 時,吞吐率隨著子線程數(shù)增加而趨向于一個穩(wěn)定的值。這個穩(wěn)定值受限于是互聯(lián)網(wǎng)絡(luò)能夠提供的最大帶寬。

其次,當(dāng)子線程數(shù)大于 16 時,吞吐率隨著子線程數(shù)增加而增加。這與互聯(lián)結(jié)構(gòu)的拓?fù)溆嘘P(guān)系。當(dāng)子線程數(shù)大于 32 時,新的子線程被分配到互聯(lián)結(jié)構(gòu)的另一半部分,從而使得能夠利用的帶寬增加。

用 align 曲線和 mutex 曲線進(jìn)行比較。當(dāng)只有 2 個線程時,無鎖 Ringbuffer 實(shí)現(xiàn)的吞吐率是基于 mutex 的 Ringbuffer 實(shí)現(xiàn)的吞吐率的 3.31 倍;當(dāng)使用 16 個線程時,無鎖 Ringbuffer 實(shí)現(xiàn)的吞吐率是基于 mutex 的 Ringbuffer 實(shí)現(xiàn)的吞吐率的 33.8 倍。當(dāng)使用 32 個線程時,無鎖 Ringbuffer 實(shí)現(xiàn)的吞吐率是基于 mutex 的 Ringbuffer 實(shí)現(xiàn)的吞吐率的 110 倍。

緩存行對齊的無鎖 Ringbuffer 實(shí)現(xiàn)的性能曲線與基于 mutex 的 Ringbuffer 實(shí)現(xiàn)和無鎖 Ringbuffer 的實(shí)現(xiàn)有本質(zhì)上的不同,呈現(xiàn)了一種互聯(lián)帶寬測試的規(guī)律。這表示緩存行對齊的無鎖 Ringbuffer 實(shí)現(xiàn)能夠充分利用互聯(lián)網(wǎng)絡(luò)提供的通信性能,是一個非常有利于系統(tǒng)規(guī)模擴(kuò)展的 Ringbuffer 實(shí)現(xiàn)。

非阻塞 Ringbuffer 實(shí)現(xiàn)

前文介紹的無鎖 Ringbuffer 實(shí)現(xiàn)存在一個重大的功能缺陷,就是上述實(shí)現(xiàn)只能構(gòu)建阻塞式的 Ringbuffer。當(dāng)生產(chǎn)者或消費(fèi)者進(jìn)入 Enqueue 或 Dequeue 函數(shù)后,生產(chǎn)者或消費(fèi)者的程序會一直嘗試操作,直到操作成功才能推出 Enqueue 或 Dequeue 函數(shù)。

在實(shí)際軟件更加傾向于實(shí)現(xiàn)非阻塞的 Ringbuffer。因?yàn)楫?dāng) Enqueue 或 Dequeue 操作不成功時,軟件可以主動進(jìn)行線程調(diào)度或休眠,將處理器資源讓渡給可以進(jìn)行操作的線程或任務(wù)。這有利于提高資源利用率。

指針保護(hù)

這里分三個步驟來推演非阻塞 Ringbuffer 的實(shí)現(xiàn)。

第一步。為了能夠?qū)崿F(xiàn)非阻塞的 Ringbuffer,那么仍然需要在 Enqueue 和 Dequeue 函數(shù)的開始的時候檢查隊(duì)列的空滿情況。如果 Ringbuffer 不滿足進(jìn)行 Enqueue 或 Dequeue 的條件,則退出函數(shù)并且返回錯誤代碼。

考慮到這一點(diǎn),非阻塞 Ringbuffer 的 Enqueue 和 Dequeue 的流程應(yīng)當(dāng)如下圖:

wKgZO2fhEkiAIiZIAACmdbxyhmg275.png

圖 14 非阻塞無鎖 Ringbuffer 實(shí)現(xiàn)的流程圖(第一步)

第二步。非阻塞實(shí)現(xiàn)要使用原子指令替代 mutex 保護(hù)指針。但是原子指令可以保護(hù)對于一個共享變量的 load-modify-store 序列,但是不能保證對于多個共享變量的 load-modify-store 之間的原子性。因此,Enqueue 中只能用原子指令保護(hù) tail 指針;Dequeue 中只能用原子指令保護(hù) head 指針。

考慮到這一點(diǎn),非阻塞 Ringbuffer 的 Enqueue 和 Dequeue 的流程應(yīng)當(dāng)如下圖:

wKgZPGfhEkiAVI8oAAD04vp1xGc859.png

圖 15 非阻塞無鎖 Ringbuffer 實(shí)現(xiàn)的流程圖(第二步)

以 enqueue 為例,函數(shù)首先讀取指針判斷空滿條件。如果 Ringbuffer 不滿,則執(zhí)行 CAS(tail_addr, tail, tail+1)。

如果 CAS 成功,則表示從判斷空滿條件到更新 tail 這段時間,沒有其他線程修改了 tail 指針,那么可以將 tail 指向的單元分配給當(dāng)前線程,并且更新 CAS。

如果 CAS 失敗,則表示從判斷空間條件到更新 tail 這段時間,其他線程修改了 tail 指針。此時,需要返回函數(shù)開始的地方重新判斷空滿條件。

這種方法可以保證 tail 指針的更新是原子的,每個生產(chǎn)者都是在最新的 tail 值基礎(chǔ)上進(jìn)行更新。

這里使用的是__atomic_compare_exchange_n原語。

bool __atomic_compare_exchange_n (type *ptr, type *expected, type desired, bool weak, int success_memorder, int failure_memorder)

__atomic_compare_exchange_n讀取ptr指向的變量,與expected指向的變量比較。如果兩者數(shù)值相同,則更新ptr指向的變量為desired,并且返回真。如果數(shù)值不同,則將讀取的數(shù)值賦值給expected指向的變量,同時返回假。

第三步。從生產(chǎn)者的角度,Enqueue 函數(shù)需要在檢查空滿條件后分配一個單元給當(dāng)前線程,并且讓其他生產(chǎn)者知道更新后的 head 或 tail 值。第二步中引入的 CAS 操作保證了這一點(diǎn)。另一方面,消費(fèi)者需要等到分配的單元被填充后才能知道指針的更新。也就是說,生產(chǎn)者和消費(fèi)者看到指針更新的時間不同。

因此,非阻塞無鎖 Ringbuffer 引入了兩組指針分別針對生產(chǎn)者和消費(fèi)者,即 prod_head/prod_tail,和 cons_head/cons_tail,如圖 15 所示。

wKgZO2fhEmGAaL8PAAEp5BejZdM431.png

圖 16 生產(chǎn)者和消費(fèi)者指針示意圖

對于生產(chǎn)者來說,其 prod_tail 指針一定是準(zhǔn)確的,但是 prod_head 指針并不是最新的。這會導(dǎo)致生產(chǎn)者認(rèn)為 Ringbuffer 中被占用的單元數(shù)多于實(shí)際被占用的單元數(shù)量,從而導(dǎo)致生產(chǎn)者錯誤地認(rèn)為隊(duì)列是滿的,而放棄 Enqueue 操作。類似地,對于消費(fèi)者來說,其 cons_head 指針一定是準(zhǔn)確的,但是 cons_tail 指針并不是最新的。那么消費(fèi)者可能認(rèn)為空閑單元少于實(shí)際的空閑單元,從而導(dǎo)致消費(fèi)者錯誤地認(rèn)為隊(duì)列是滿的,而放棄 Dequeue 操作。這樣的誤導(dǎo)判斷雖然會導(dǎo)致生產(chǎn)者和消費(fèi)者對于 Ringbuffer 的競爭比較保守,結(jié)果是 Ringbuffer 效率降低,但是不會引起功能錯誤。

考慮到這一點(diǎn),非阻塞 Ringbuffer 的 Enqueue 和 Dequeue 的流程應(yīng)當(dāng)如下圖:

wKgZPGfhEmGAY6lbAAEdu6Svrv4450.png

圖 17 非阻塞無鎖 Ringbuffer 實(shí)現(xiàn)的流程圖(第三步)

生產(chǎn)者和消費(fèi)者的指針同步

當(dāng) Enqueue 操作完成時,需要將生產(chǎn)者視角的 prod_tail 指針同步到消費(fèi)者視角的 cons_tail 指針,告知消費(fèi)者這些單元已經(jīng)完成了 Enqueue 操作;當(dāng) Dequeue 操作完成時,需要將消費(fèi)者的 cons_head 指針同步到消費(fèi)者的 prod_head 指針,告知生產(chǎn)者這些單元已經(jīng)完成了 Dequeue 操作。

但是這并不能夠天然保證的。因?yàn)槌酸槍?prod_tail 和 cons_head 指針的 CAS 操作,Enqueue 和 Dequeue 函數(shù)的其他部分都是并行,如圖 18 所示。因此,某個線程的分配單元完成 Enqueue 或 Dequeue 操作時,完全不能推測這個單元之前的其他單元是否完成了 Enqueue 或 Dequeue 的操作。

wKgZO2fhEmGAHlJfAABy2Bd-m6w709.png

圖 18 非阻塞無鎖 Ringbuffer 的執(zhí)行時序示意圖

指針同步方式有很多種實(shí)現(xiàn)方法,主要分為順序釋放和亂序釋放兩種實(shí)現(xiàn)方法。

順序釋放。要求同步到另一個視角的單元必須完成了 Enqueue 或 Dequeue 操作。這就需要在更新指針前對于單元進(jìn)行檢查,只能將操作完成的單元可以同步給另一個視角。

比如,某個線程的 Enqueue 函數(shù)分配的單元為 16,此時 cons_tail 的值是 10,則 Enqueue 函數(shù)不能更新 cons_tail。只有 cons_tail 的值是 15 時才能更新 cons_tail。這樣才能保證之前的物理單元都已經(jīng)完成了 Enqueue 操作。

為了保證指針操作的原子性,需要使用__atomic_compare_exchange_n原語更新另一個視角的指針。

亂序釋放。在同步不同視角的指針時,不檢查單元是否已經(jīng)完全寫入完成,而是直接將指針加 1。這就會導(dǎo)致在后續(xù) Enqueue 或 Dequeue 種分配的單元不能滿足操作的要求。

比如,某個線程的 Enqueue 函數(shù)分配的單元為 16,此時 cons_tail 的值是 10。當(dāng)這個 Enqueue 函數(shù)完成時將 cons_tail 更新為 11,但是此時對于物理單元 11 的操作狀態(tài)未知。物理單元 11 有可能已經(jīng)完成了 Enqueue 操作,也有可能物理單元 11 還是空的。

為了避免功能錯誤,則需要在操作分配的單元時進(jìn)行檢查。對于 Enqueue,如果分配的單元不為空,則一直等待;類似地,對于 Dequeue,如果分配的單元為空,則也要一直等待。

為了保證指針操作的原子性,需要使用__atomic_fetch_add原語更新另一個視角的指針。

至此,我們推導(dǎo)出了,非阻塞 Ringbuffer 的 Enqueue 和 Dequeue 的完整流程,如圖 19 所示。

wKgZPGfhEmGAZJQ9AAFxNei0Eoo428.png

圖 19 非阻塞無鎖 Ringbuffer 實(shí)現(xiàn)的流程圖

Ringbuffer 聲明

非阻塞的無鎖 Ringbuffer 不需要序列號。每個數(shù)據(jù)單元中只有一個指向?qū)嶋H數(shù)據(jù)結(jié)構(gòu)的指針。

// Entry in ring buffer
class BufferEntry
{
public:
    void *mp_ptr; // Pointer to data entry.
};


class RingBuffer
{
public:
    alignas(64) unsigned int m_size;
 
    // Pointers in the view of producer.
    alignas(64) unsigned int m_prod_head; // read pointer
    alignas(64) unsigned int m_prod_tail; // write pointer
 
    // Pointers in the view of consumer.
    alignas(64) unsigned int m_cons_head; // write pointer
    alignas(64) unsigned int m_cons_tail; // read pointer
 
    alignas(64) BufferEntry *mp_entries;
};

代碼 7 非阻塞無鎖 Ringbuffer 的聲明

Enqueue 和 Dequeue

本文測試的非阻塞 Ringbuffer 實(shí)現(xiàn)采用了亂序釋放的方法。

 int enqueue_ringbuf(RingBuffer *ring_buffer, void *entry)
{
    // Step 1: acquire slots
    unsigned int tail = __atomic_load_n(&ring_buffer->m_prod_tail, __ATOMIC_RELAXED);
    do
    {
        unsigned int head = __atomic_load_n(
            &ring_buffer->m_prod_head, __ATOMIC_ACQUIRE);
        if (ring_buffer->m_size + head == tail)
        {
            return 1;
        }
    } while (!__atomic_compare_exchange_n(&ring_buffer->m_prod_tail,
                                          &tail, // Updated on failure
                                          tail + 1,
                                          /*weak=*/true,
                                          __ATOMIC_RELAXED,
                                          __ATOMIC_RELAXED));
 
    // Step 2: Write slots
    int enq_ptr = (tail % ring_buffer->m_size) * 8;
    void *entry_ptr;
    do
    {
        entry_ptr = __atomic_load_n(&ring_buffer->mp_entries[enq_ptr].mp_ptr, __ATOMIC_RELAXED);
    } while (entry_ptr != NULL);
 
    __atomic_store_n(&ring_buffer->mp_entries[enq_ptr].mp_ptr, entry, __ATOMIC_RELEASE);
 
    // Finally make all released slots available for new acquisitions
    __atomic_fetch_add(&ring_buffer->m_cons_tail, 1, __ATOMIC_RELEASE);
    return 0;
}
 
int dequeue_ringbuf(RingBuffer *ring_buffer, void **entry)
{
    // Step 1: acquire slots
    unsigned int tail = __atomic_load_n(&ring_buffer->m_cons_head, __ATOMIC_RELAXED);
    do
    {
        unsigned int head = __atomic_load_n(
            &ring_buffer->m_cons_tail, __ATOMIC_ACQUIRE);
        if (head == tail)
        {
            return 1;
        }
    } while (!__atomic_compare_exchange_n(&ring_buffer->m_cons_head,
                                          &tail, // Updated on failure
                                          tail + 1,
                                          /*weak=*/true,
                                          __ATOMIC_RELAXED,
                                          __ATOMIC_RELAXED));
 
    // Step 2: Write slots (write NIL for dequeue)
    int deq_ptr = (tail % ring_buffer->m_size) * 8;
    unsigned int entry_sn;
    void *entry_ptr;
    do
    {
        entry_ptr = __atomic_load_n(&ring_buffer->mp_entries[deq_ptr].mp_ptr, __ATOMIC_ACQUIRE);
    } while (entry_ptr == NULL);
 
    *entry = entry_ptr;
    __atomic_store_n(&ring_buffer->mp_entries[deq_ptr].mp_ptr, 0, __ATOMIC_RELAXED);
 
    // Finally make all released slots available for new acquisitions
    __atomic_fetch_add(&ring_buffer->m_prod_head, 1, __ATOMIC_RELEASE);
    return 0;
}

代碼 8 非阻塞無鎖 Ringbuffer 的實(shí)現(xiàn)

非阻塞的無鎖 ringbuffer 實(shí)現(xiàn)的完整源碼請參見(https://github.com/wangeddie67/ringbuffer_opt_demo/blob/main/srcs/buck_blkring.h)。

非阻塞 Ringbuffer 性能測試結(jié)果

非阻塞的無鎖 Ringbuffer 的性能如圖 13 所示。曲線 mutex 表示基于 mutex 的 Ringbuffer 實(shí)現(xiàn);lockfree 和 atomic 表示無鎖 Ringbuffer 實(shí)現(xiàn)(lockfree 使用普通 load/store 操作數(shù)據(jù)單元;atomic 使用原子 load/store 操作數(shù)據(jù)單元);align 表示緩存行對齊后的無鎖 Ringbuffer 實(shí)現(xiàn);buck 表示的非阻塞無鎖 Ringbuffer 實(shí)現(xiàn)。

wKgZO2fhEnmAWpruAAJHxcFyoCU633.png

圖 20 非阻塞無鎖 Ringbuffer 性能測試

非阻塞的無鎖 Ringbuffer 的性能曲線呈現(xiàn)兩段趨勢:

從 2 個線程到 16 個線程左右,吞吐率隨著線程數(shù)增加而緩慢增加。

從 16 個線程左右到 32 個線程,吞吐率隨著線程數(shù)增加而緩慢降低。

與基于 mutex 的 Ringbuffer 相比,非阻塞 Ringbuffer 的性能要遠(yuǎn)好于基于 mutex 的 Ringbuffer 的性能,并且在使用 16-20 個線程時達(dá)到最高。

另一方面,與緩存行對齊的無鎖 Ringbuffer 實(shí)現(xiàn)相比,非阻塞 Ringbuffer 的性能差強(qiáng)人意。這是因?yàn)榉亲枞?Ringbuffer 引入了更多的指針競爭。阻塞的無鎖 Ringbuffer 實(shí)現(xiàn)的 Enqueue 和 Dequeue 中只關(guān)注一個共享指針。但是非阻塞 Ringbuffer 實(shí)現(xiàn)的 Enqueue 和 Dequeue 則需要關(guān)注三個共享指針。這就導(dǎo)致出現(xiàn)了更多的共享變量的競爭,從而引起了性能下降。

取 2 個線程、16 個線程和 32 個線程三個位置進(jìn)行定量比較,得到下表。表中第一行是吞吐率數(shù)據(jù)(單元 ops/s),第二行是相對于基準(zhǔn)(基于 mutex 的 Ringbuffer 實(shí)現(xiàn))的加速比。

wKgZPGfhEnmAUiHdAAGf78uCHC0188.png

相對于基于 mutex 的 Ringbuffer,非阻塞的無鎖 Ringbuffer 可以取得超過 10 倍的吞吐率提升。這表示非阻塞的無鎖 Ringbuffer 實(shí)現(xiàn)仍然是非常有效的,值得在實(shí)際應(yīng)用中嘗試。

與 program64 的性能比較

在文系列完整的開篇處已經(jīng)聲明,本系列文章介紹的所有優(yōu)化來自于 Ola 的研究。Ola 開發(fā)的 program64 庫也是示例的 Ringbuffer 實(shí)現(xiàn)的基礎(chǔ)。

無鎖 Ringbuffer 實(shí)現(xiàn)對應(yīng)于 program64 庫中的 p64_blkring 實(shí)現(xiàn)。

非阻塞 Ringbuffer 實(shí)現(xiàn)對應(yīng)于 program64 庫中的 p64_buckring 實(shí)現(xiàn)。

這里將 program64 庫的實(shí)現(xiàn)集成到我們的測試程序中,使用相同的測試環(huán)境和配置進(jìn)行了測試。align 和 p64_blkring 是一個對照組;buck 和 p64_buckring 是一個對照組。

wKgZO2fhEnmAR3VrAAJx9ET7jeo719.png

圖 21 在 Arm 平臺上與 Program64 庫對比性能

我們的示例代碼的性能與 program64 的性能在趨勢上是一致,并且在數(shù)值范圍上基本重合。兩者之間的差距則是由于實(shí)現(xiàn)的細(xì)節(jié)引入的。比如 program64 的 Enqueue 和 Dequeue 可以操作多個單元,而示例代碼只能操作一個單元。這些差距并不影響關(guān)于 Ringbuffer 優(yōu)化的結(jié)論。

結(jié)論

本文介紹了多種 Ringbuffer 實(shí)現(xiàn),并且比較和分析了不同 Ringbuffer 實(shí)現(xiàn)的性能表現(xiàn):

基于 mutex 的 Ringbuffer 性能主要受到多線程相互競爭 mutex 的影響。參與競爭的線程越多,Ringbuffer 的吞吐率越低,非常不利于系統(tǒng)規(guī)模的擴(kuò)展。

阻塞式的無鎖 Ringbuffer 能夠從原子操作和緩存行對齊兩個優(yōu)化中獲得非常明顯的收益,使得實(shí)現(xiàn)能夠充分利用系統(tǒng)中互聯(lián)系統(tǒng)提供的通信容量,是一種非常有利于系統(tǒng)規(guī)模擴(kuò)展的 Ringbuffer 實(shí)現(xiàn)。

非阻塞 Ringbuffer 實(shí)現(xiàn)豐富并完善了 Ringbuffer 的功能,并且同樣可以取得很好的性能優(yōu)化。但是由于引入了更多的共享變量,非阻塞 Ringbuffer 實(shí)現(xiàn)的性能要弱于緩存行對齊的無鎖 Ringbuffer 實(shí)現(xiàn)。

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報(bào)投訴
  • ARM
    ARM
    +關(guān)注

    關(guān)注

    134

    文章

    9235

    瀏覽量

    371734
  • 服務(wù)器
    +關(guān)注

    關(guān)注

    12

    文章

    9485

    瀏覽量

    86639
  • 軟件
    +關(guān)注

    關(guān)注

    69

    文章

    5071

    瀏覽量

    88534
  • 數(shù)組
    +關(guān)注

    關(guān)注

    1

    文章

    419

    瀏覽量

    26158

原文標(biāo)題:在 Arm 平臺上實(shí)現(xiàn)性能可擴(kuò)展的 Ringbuffer

文章出處:【微信號:Ithingedu,微信公眾號:安芯教育科技】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

收藏 0人收藏

    評論

    相關(guān)推薦

    C/C++性能優(yōu)化背后的方法論:TMAM

    開發(fā)過程中我們多少都會關(guān)注服務(wù)的性能,然而性能優(yōu)化是相對比較困難,往往需要多輪優(yōu)化、測試,屬于費(fèi)時費(fèi)力,有時候還未必有好的效果。但是如果有較好的性能
    發(fā)表于 11-04 08:56 ?1052次閱讀

    【獨(dú)風(fēng)科創(chuàng)】RingBuffer開源C語言組件發(fā)布啦

    09:00]期望幀邏輯優(yōu)化修改匹配期望幀任務(wù)的條件增加匹配期望幀函數(shù)1.0.0.1優(yōu)化耦合性優(yōu)化讀取邏輯增加多緩沖區(qū)支持1.0.0.0主體架構(gòu)搭建,完成讀寫環(huán)形結(jié)構(gòu)化【RingBuffer
    發(fā)表于 03-09 14:36

    HBase性能優(yōu)化方法總結(jié)

    對于寫密集型提高性能需盡量減少刷寫、合并和拆分的次數(shù),以減少IO壓力,提高系統(tǒng)性能。除了以上方法可以提高HBase性能之外,還可以采用以下方法
    發(fā)表于 04-20 17:16

    Linux和Android系統(tǒng)故障和優(yōu)化性能方法和流程探討

    優(yōu)化變得異常復(fù)雜,如何定位性能問題出在哪個方面,是性能優(yōu)化的一大難題, 從系統(tǒng)入手,闡述由于系統(tǒng)軟、硬件配置不當(dāng)可能造成的性能問題,并且探
    發(fā)表于 07-22 06:48

    差動放大器的性能優(yōu)化方法

    的使用。下面就來分享構(gòu)建差動放大器及其性能優(yōu)化方法!儀表放大器可能不具備用戶要求的帶寬、直流精度或功耗。因而,在這種情況下,用戶可通過一個單放大器和外部電阻自行構(gòu)建差分放大器,以替代儀表放大器。不過,除非
    發(fā)表于 07-24 06:36

    AN0004—AT32 性能優(yōu)化

    本帖最后由 貪玩 于 2022-2-16 21:42 編輯 AN0004—AT32 性能優(yōu)化這篇應(yīng)用筆記描述了如何通過軟件方法提高AT32的運(yùn)行效能。AT32 性能
    發(fā)表于 08-15 14:38

    使用RingBuffer處理數(shù)據(jù)

    嵌入式硬件通信接口:使用RingBuffer處理數(shù)據(jù)(二)詳細(xì)設(shè)計(jì)過程
    發(fā)表于 01-13 06:02

    如何設(shè)計(jì)ringbuffer

    如何設(shè)計(jì)ringbuffer
    發(fā)表于 11-10 08:38

    求助大神,rt_ringbuffer_peak疑問求解

    rt_size_t rt_ringbuffer_peak(struct rt_ringbuffer *rb, rt_uint8_t **ptr){ RT_ASSERT(rb != RT_NULL
    發(fā)表于 05-27 11:30

    《現(xiàn)代CPU性能分析與優(yōu)化》---精簡的優(yōu)化

    《現(xiàn)代CPU性能分析與優(yōu)化》是一本非常實(shí)用的書籍,對于從事性能關(guān)鍵型應(yīng)用程序開發(fā)和進(jìn)行系統(tǒng)底層優(yōu)化的技術(shù)人員來說是不可或缺的。這本書也很適合任何想更好地了解應(yīng)用程序
    發(fā)表于 04-18 16:03

    RT-Thread文檔_ringbuffer

    RT-Thread文檔_ringbuffer
    發(fā)表于 02-22 18:40 ?3次下載
    RT-Thread文檔_<b class='flag-5'>ringbuffer</b>

    ringbuffer數(shù)據(jù)結(jié)構(gòu)介紹

    開發(fā)人員忽略的。在整個通信協(xié)議的開發(fā)團(tuán)隊(duì)中,一般會有一個平臺中間件的團(tuán)隊(duì),他們的任務(wù)是給業(yè)務(wù)部門提供高性能、高可靠性的中間件代碼,如內(nèi)存池、線程池、消息通信機(jī)制、日志系統(tǒng)等等。這篇文章就來討論下這個簡約而不簡單的ringbuffer
    的頭像 發(fā)表于 11-13 10:44 ?1819次閱讀
    <b class='flag-5'>ringbuffer</b>數(shù)據(jù)結(jié)構(gòu)介紹

    MySQL性能優(yōu)化方法

    MySQL 性能優(yōu)化是一項(xiàng)關(guān)鍵的任務(wù),可以提高數(shù)據(jù)庫的運(yùn)行速度和效率。以下是一些優(yōu)化方法,包括具體代碼和詳細(xì)優(yōu)化方案。
    的頭像 發(fā)表于 11-22 09:59 ?752次閱讀

    差動放大器的性能優(yōu)化方法

    電子發(fā)燒友網(wǎng)站提供《差動放大器的性能優(yōu)化方法.pdf》資料免費(fèi)下載
    發(fā)表于 11-23 14:32 ?0次下載
    差動放大器的<b class='flag-5'>性能</b><b class='flag-5'>優(yōu)化</b><b class='flag-5'>方法</b>

    AI大模型的性能優(yōu)化方法

    AI大模型的性能優(yōu)化是一個復(fù)雜而關(guān)鍵的任務(wù),涉及多個方面和策略。以下是一些主要的性能優(yōu)化方法: 一、模型壓縮與
    的頭像 發(fā)表于 10-23 15:01 ?1625次閱讀
    主站蜘蛛池模板: 益日韩欧群交P片内射中文 艺术片 快播 | 桃花免费高清在线观看 | 亚洲免费三级电影 | 国产精品人妻无码久久久蜜桃 | 99久久精品国产一区二区三区 | 免费国产成人高清在线看软件 | 中国老太婆xxxxx | 久久99国产亚洲高清观着 | 三级网站视频 | 亚洲欧美中文日韩v在线 | 欧美一道本一区二区三区 | 美女被爆羞羞天美传媒 | 麻豆免费观看高清完整视频在线 | 久久www免费人成高清 | 野花韩国高清完整版在线观看5 | JIZJIZJIZ 日本老师水多 | 成人五级毛片免费播放 | 亚洲国产在线播放在线 | 中国老太性色xxxxxhd | av女优快播 | 乌克兰10一12x video | 亚洲色无码播放 | 麻美ゆま夫の目の前で犯 | 免费国产久久啪久久爱 | 同桌上课把奶露出来给我玩 | 国产高清视频在线观看不卡v | SM高H黄暴NP辣H调教性奴 | 国内极度色诱视频网站 | 亚洲 日韩 欧美 另类 蜜桃 | 国产亚洲美女精品久久久2020 | 一本色道久久综合亚洲精品 | 久久中文字幕亚洲 | 亲胸吻胸添奶头GIF动态图免费 | 24小时日本在线观看片免费 | ae58老司机福利 | 99久久精品一区二区三区 | 动漫女主被扒开双腿羞辱 | 乱精品一区字幕二区 | 亚洲看片无码免费视频 | 久久天天躁狠狠躁夜夜躁 | 国内精品自产拍在线少密芽 |

    電子發(fā)燒友

    中國電子工程師最喜歡的網(wǎng)站

    • 2931785位工程師會員交流學(xué)習(xí)
    • 獲取您個性化的科技前沿技術(shù)信息
    • 參加活動獲取豐厚的禮品