網站首頁 編程語言 正文
背景
環形緩沖器(ringr buffer)是一種用于表示一個固定尺寸、頭尾相連的緩沖區的數據結構,適合緩存數據流。
在使用上,它就是一個固定長度的FIFO隊列:
在邏輯上,我們可以把它當成是一個環,上面有兩個指針代表當前寫索引和讀索引:
在實現上,我們一般是使用一個數組去實現這個環,當索引到達數組尾部的時候,則重新設置為頭部:
kfifo實現
kfifo是Linux內核的隊列實現,它具有以下特性:
- 固定長度:長度是固定的,而且是向上取最小的2的平方,主要是為了實現快速取余。
- 無鎖:在單生產者和單消費者的情況下,是不需要加鎖的。主要是因為索引in和out是不回退的,一直往前。
- 快速取余:我們都直到到達隊列末尾的時候,索引需要回退到開頭。最簡單的實現方式就是對索引取余,比如索引in現在是8,隊列長度是8,
in%len(q)
即可回退到開頭,但是取余操作%
還是比較耗時的,因此kfifo使用in&mask
實現快速取余,其中mask=len(q)-1
。
無鎖
上面我們說到,這個無鎖是有條件的,也就是必須在單生產者單消費者情況下。這種情況下,同一時刻最多只可能會有一個寫操作和一個讀操作。但是在某一個讀操作(或寫操作)的期間,可能會有多個寫操作(或讀操作)發生。
因為索引in和out是不回退的,因此in一直會在out前面(或者重合)。而且in只被寫操作修改,out只被讀操作修改,因此不會沖突。
這里可能有人會擔心索引溢出的問題,比如in到達math.MaxUint64,再+1則回到0。但是其實并不影響in和out之間的距離:
package main import ( "fmt" "math" ) func main() { var in uint = math.MaxUint64 var out uint = math.MaxUint64 - 1 fmt.Println(in - out) // 1 in++ fmt.Println(in - out) // 2 out++ fmt.Println(in - out) // 1 }
當然如果連續兩次溢出,就會出現問題。但是由于數組長度是int類型,因此也沒辦法超過math.MaxUint64
,也就是in和out之間的距離最多也就是2^62
,因為math.MaxInt64
是2^63-1
,沒辦法向上取2的平方了。因此也不會出現溢出兩倍math.MaxUint64
的情況,早在溢出之前就隊列滿了。
快速取余
前面提到取余是通過in&mask
實現的,這有一個前提條件,也就是長度必須是2的次方,因此在創建數組的時候,長度會向上取最小的2的平方。例如一個長度為8
的kfifo,在二進制表示下:
len = 0000 1000 // 十進制8,隊列長度 mask = 0000 0111 // 十進制7,掩碼 in = 0000 0000 // 十進制0,寫索引 in & mask => 0000 0000 // 十進制0,使用 & mask in % len => 0000 0000 // 十進制0,使用 % len in = 0000 0001 // 十進制1,寫索引 in & mask => 0000 0001 // 十進制1,使用 & mask in % len => 0000 0001 // 十進制1,使用 % len in = 0000 0001 // 十進制1,寫索引 in & mask => 0000 0001 // 十進制1,使用 & mask in % len => 0000 0001 // 十進制1,使用 % len in = 0000 1000 // 十進制8,寫索引 in & mask => 0000 0000 // 十進制0,使用 & mask in % len => 0000 0000 // 十進制0,使用 % len in = 0001 0001 // 十進制17,寫索引 in & mask => 0000 0001 // 十進制1,使用 & mask in % len => 0000 0001 // 十進制1,使用 % len
可以看到,使用& mask
的效果是和% len
一樣的。
然后我們做一個簡單的性能測試:
package main import "testing" var ( Len = 8 Mask = Len - 1 In = 8 - 5 ) // % len func BenchmarkModLen(b *testing.B) { for i := 0; i < b.N; i++ { _ = In % Len } } // & Mask func BenchmarkAndMask(b *testing.B) { for i := 0; i < b.N; i++ { _ = In & Mask } }
測試結果:
BenchmarkModLen-8 ? ? ? 1000000000 ? ? ? ? ? ? ? 0.3434 ns/op
BenchmarkAndMask-8 ? ? ?1000000000 ? ? ? ? ? ? ? 0.2520 ns/op
可以看到& mask
性能確實比% len
好很多,這也就是為什么要用& Mask
來實現取余的原因了。
數據結構
數據結構和上面介紹的一樣,in、out標識當前讀寫的位置;mask是size-1,用于取索引,比%size
更加高效;
type Ring[T any] struct { in uint64 // 寫索引 out uint64 // 讀索引 mask uint64 // 掩碼,用于取索引,代替%size size uint64 // 長度 data []T // 數據 }
Push()
Push()操作很簡單,首先r.in & r.mask
得到寫索引,讓寫索引前進一格,然后存入數據。
// 插入元素到隊尾 func (r *Ring[T]) Push(e T) { if r.Full() { panic("ring full") } in := r.in & r.mask r.in++ r.data[in] = e }
Pop()
Pop()操作同理,根據r.out & r.mask
得到讀索引,讓讀索引前進一格,然后讀取數據。
// 彈出隊頭元素 func (r *Ring[T]) Pop() T { if r.Empty() { panic("ring emtpy") } out := r.out & r.mask r.out++ return r.data[out] }
性能測試
Round實現是使用& mask
,同時長度會向上取2的平方;Fix實現是使用% size
保持參數的長度。
測試代碼是不斷的Push()然后Pop():
func BenchmarkRoundPushPop(b *testing.B) { for i := 0; i < b.N; i++ { r := New[int](RoundFixSize) for j := 0; j < RoundFixSize; j++ { r.Push(j) } for j := 0; j < RoundFixSize; j++ { r.Pop() } } }
測試結果:& mask
的性能明顯好于% size
。
BenchmarkRoundPushPop-8 ? ? ? ? ? ? 2544 ? ? ? ? ? ?405621 ns/op // & mask
BenchmarkFixPushPop-8 ? ? ? ? ? ? ? ?678 ? ? ? ? ? 1740489 ns/op // % size
無界環形緩沖器
我們可以在寫數據的時候判斷是否空間已滿,如果已滿我們可以進行動態擴容,從而實現一個無界環形緩沖器。
Push()
在Push()時檢查到空間滿時,調用grow()擴展空間即可:
// 插入元素到隊尾 func (r *Ring[T]) Push(e T) { if r.Full() { // 擴展空間 r.Grow(r.Cap() + 1) } in := r.in % r.size r.in++ r.data[in] = e }
grow()
擴容一般是擴展為當前容量的兩倍,然后把原來數據copy()到新的數組,更新字段即可:
// 擴容 func (r *Ring[T]) Grow(minSize uint64) { size := mmath.Max(r.size*2, minSize) if size > MaxSize { panic("size is too large") } if size < 2 { size = 2 } // 還沒容量,直接申請,因為不需要遷移元素 if r.size == 0 { r.data = make([]T, size) r.size = size return } data := make([]T, size) out := r.out % r.size len := r.Len() copied := copy(data[:len], r.data[out:]) copy(data[copied:len], r.data) r.out = 0 r.in = len r.size = size r.data = data }
線程安全性
由于可能會動態擴容,需要修改out、in指針,因此需要加鎖保證安全。
代碼地址
https://github.com/jiaxwu/gommon/tree/main/container/ringbuffer
原文鏈接:https://juejin.cn/post/7138675261649715236
相關推薦
- 2023-07-07 sklearn.model_selection模塊介紹
- 2022-12-15 C++?Boost?MPI接口詳細講解_C 語言
- 2022-08-25 python并發場景鎖的使用方法_python
- 2022-07-09 docker 中進程的信號
- 2023-10-09 注冊頁面編寫
- 2023-03-28 python中向二維數組中添加整行或者增列元素問題_python
- 2022-12-14 Python如何對音視頻文件進行解析詳解_python
- 2022-07-10 詳解BlockingQueue阻塞隊列的使用
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支