操作到高性能集合運算與序列化)
在 Go 中使用 bitset 位集從基礎(chǔ)操作到高性能集合運算與序列化【免費下載鏈接】lokiLike Prometheus, but for logs.項目地址: https://gitcode.com/GitHub_Trending/lok/loki導(dǎo)讀bitset是 Go 語言生態(tài)中用于「非負(fù)整數(shù) → 布爾值」映射的高性能位集庫相比map[uint]bool在內(nèi)存占用與運算速度上均有數(shù)量級優(yōu)勢。本倉庫Grafana Loki以依賴形式引入了github.com/bits-and-blooms/bitsetv1.25.0在布隆過濾器、索引與查詢的位級過濾等場景中作為底層數(shù)據(jù)結(jié)構(gòu)使用。閱讀本文后你將掌握位集的創(chuàng)建、增刪改查、集合運算、遍歷、序列化與并發(fā)安全邊界并能直接在 Go 項目中落地這套方案。位集是什么為什么比map[uint]bool更高效包級文檔對位集的定義非常直白Package bitset implements bitsets, a mapping between non-negative integers and boolean values. It should be more efficient than map[uint] bool.其核心思路是把布爾值按位緊湊地存儲而不是為每個元素單獨分配一個 bool。從源碼看BitSet的內(nèi)部結(jié)構(gòu)只有兩個字段vendor/github.com/bits-and-blooms/bitset/bitset.go#L86-L90const wordSize 64 const wordBytes wordSize / 8 type BitSet struct { length uint set []uint64 }即底層是[]uint64數(shù)組每個 64 位字word可表示 64 個布爾位length記錄當(dāng)前邏輯位長度。因此「N 位所需內(nèi)存至少為 N/8 字節(jié)」且數(shù)組只增長到「最大已設(shè)置位的下標(biāo) 1」對應(yīng)的字?jǐn)?shù)按需分配extendSet負(fù)責(zé)擴容。相比map[uint]bool每個條目至少占用一個指針槽位和一個 bool 值通常數(shù)十字節(jié)位集在密集整型集合場景下內(nèi)存可縮減一個數(shù)量級以上同時得益于math/bits的硬件指令級位運算見popcnt.go中基于bits.OnesCount64的種群計數(shù)實現(xiàn)集合基數(shù)統(tǒng)計Count與各類集合運算都能以 word 粒度并行推進。Loki 倉庫在 go.mod#L185 中以間接依賴形式引入github.com/bits-and-blooms/bitset v1.25.0位集正是這類日志索引/過濾系統(tǒng)中布隆過濾器等位密集型數(shù)據(jù)結(jié)構(gòu)的常用底層載體??焖偕鲜职惭b與第一個示例安裝方式v1.25.0 對應(yīng)本倉庫 vendor 目錄中鎖定的版本go get github.com/bits-and-blooms/bitset原文檔給出了一個非常經(jīng)典的「Go Fish」示例——用它模擬抽牌與配對判斷同時演示Set、Test、Clear三個最基礎(chǔ)的原子操作package main import ( fmt math/rand github.com/bits-and-blooms/bitset ) func main() { fmt.Printf(Hello from BitSet!\n) var b bitset.BitSet // play some Go Fish for i : 0; i 100; i { card1 : uint(rand.Intn(52)) card2 : uint(rand.Intn(52)) b.Set(card1) if b.Test(card2) { fmt.Println(Go Fish!) } b.Clear(card1) } }注意這里var b bitset.BitSet直接使用了零值——文檔與源碼均明確「零值即長度為 0 的空集合」safeSet會在首次使用時自動將set初始化為非 nilbitset.go#L95-L101。創(chuàng)建帶初始容量提示的位集使用bitset.New(length)其實現(xiàn)為make([]uint64, wordsNeeded(length))即按(length63)/64個字預(yù)分配避免后續(xù)頻繁擴容。此外還有MustNewpanic 版本、From/FromWithLength從既有[]uint64字?jǐn)?shù)組直接構(gòu)造適合高級用戶零拷貝復(fù)用內(nèi)存等構(gòu)造函數(shù)。核心操作速查設(shè)置、清除、翻轉(zhuǎn)與測試位集對單個整數(shù)提供四類最基礎(chǔ)的原子方法且Set、Clear、Flip返回*BitSet支持鏈?zhǔn)秸{(diào)用方法行為返回值Set(i uint)將第 i 位置 1*BitSet可鏈?zhǔn)紺lear(i uint)將第 i 位清 0*BitSet可鏈?zhǔn)絊etTo(i uint, value bool)按布爾值設(shè)置第 i 位*BitSetFlip(i uint)翻轉(zhuǎn)第 i 位*BitSet可鏈?zhǔn)絋est(i uint)測試第 i 位是否為 1boolLen()返回當(dāng)前位集長度最大下標(biāo)1uintCount()返回置 1 的位數(shù)基數(shù)uint從實現(xiàn)看Test通過wordsIndex(i)定位字、uint64(1) (i wordMask)計算掩碼后做與運算Set則先extendSet(i)確保容量足夠再對目標(biāo)字做或運算。鏈?zhǔn)秸{(diào)用的典型用法b.Set(10).Set(11) // 同時設(shè)置第 10、11 位 b.Flip(3).Clear(7) // 翻轉(zhuǎn)第 3 位再清除第 7 位 if b.Test(10) { // 判斷第 10 位是否被設(shè)置 // ... }此外還有面向區(qū)間的SetRange(start, end)、FlipRange(start, end)以及全量操作的SetAll()/ClearAll()。針對長度收縮Shrink(lastbitindex)可以按給定下標(biāo)裁剪位集Compact()則會裁剪尾部多余的零字——文檔明確位集「從不自動收縮」在高頻增刪場景下這兩個方法用于手動歸還內(nèi)存。遍歷置 1 的位有兩種方式。經(jīng)典寫法配合NextSet從指定起點向后掃描for i, e : b.NextSet(0); e; i, e b.NextSet(i1) { fmt.Println(The following bit is set:, i) }如果使用 Go 1.23 及以上則可以直接用 range-over-func 語法for i : range b.EachSet() {}EachSet定義在 vendor/github.com/bits-and-blooms/bitset/bitset_iter.go#L19-L31它以bits.TrailingZeros64逐字跳過連續(xù) 0 位按升序 yield 每個置 1 位的下標(biāo)提前 break 會停止迭代。該文件帶有//go:build go1.23構(gòu)建標(biāo)簽因此只在 Go 1.23 編譯環(huán)境中生效。反向遍歷則可用PreviousSet/PreviousClear。對于需要批量消費的場景NextSetMany(i, buffer)可以一次填充一個[]uint緩沖減少逐位調(diào)用開銷。集合運算交集、并集、差集、補集與對稱差位集真正的價值在于把集合運算轉(zhuǎn)化為 word 級位的按位與/或/異或/取反這是map無法比擬的。完整的方法族如下運算返回新集合返回基數(shù)原地修改交集Intersection(other)IntersectionCardinality(other)InPlaceIntersection(other)并集Union(other)UnionCardinality(other)InPlaceUnion(other)差集Difference(other)DifferenceCardinality(other)InPlaceDifference(other)對稱差SymmetricDifference(other)SymmetricDifferenceCardinality(other)InPlaceSymmetricDifference(other)補集Complement()——原文檔中的示例驗證了交集語義if b.Intersection(bitset.New(100).Set(10)).Count() 1 { fmt.Println(Intersection works.) } else { fmt.Println(Intersection doesnt work???) }實現(xiàn)細(xì)節(jié)上bitset.go#L1010-L1065 附近返回新集合的版本會先按長度對兩個操作數(shù)排序再以較短的集合為基準(zhǔn)進行位運算從而減少遍歷字?jǐn)?shù)原地版本則把結(jié)果寫回調(diào)用者。基數(shù)版本如IntersectionCardinality不會物化中間結(jié)果直接逐字bits.OnesCount64累加適合「只想知道交疊數(shù)量」的判斷場景——例如布隆過濾器多塊之間做存在性驗證時只關(guān)心交集是否非空。集合查詢方法還包括Any()— 是否存在置 1 的位All()— 是否全部位均為 1None()— 是否沒有任何置 1 的位IsSuperSet(other)/IsStrictSuperSet(other)— 是否為嚴(yán)格超集Equal(other)— 兩個位集是否相等Clone()/Copy(c)/CopyFull(c)— 拷貝Copy返回被復(fù)制的位數(shù)CopyFull保證長度一致Rank(index)/Select(index)— 前 index 位的置 1 計數(shù) / 第 index 個置 1 位的下標(biāo)見 bitset.go#L1459-L1498是位集上「雙向映射」的經(jīng)典加速手段DumpAsBits()— 以 0/1 字符串輸出全部位便于調(diào)試另一個值得一提的高級接口是Words()替代已廢棄的Bytes()與SetBitsetFrom(buf []uint64)前者直接暴露內(nèi)部[]uint64字?jǐn)?shù)組非拷貝改動會影響位集后者可用外部字?jǐn)?shù)組就地填充位集兩者均標(biāo)注「面向高級用戶」可用于零拷貝集成其他位級結(jié)構(gòu)。序列化WriteTo / ReadFrom 與編碼選項位集可以安全、可移植地序列化為字節(jié)流。寫入的典型模式原文檔示例const length 9585 const oneEvery 97 bs : bitset.New(length) // Add some bits for i : uint(0); i length; i oneEvery { bs bs.Set(i) } var buf bytes.Buffer n, err : bs.WriteTo(buf) if err ! nil { // failure } // Here n buf.Len()讀取回來// Read back from buf bs bitset.New() n, err bs.ReadFrom(buf) if err ! nil { // error } // n is the number of bytes read從實現(xiàn)看bitset.go#L1332-L1405WriteTo的流格式為先寫一個uint64長度按當(dāng)前字節(jié)序隨后寫wordCount()個 64 位字ReadFrom反向讀取若當(dāng)前實例容量不足會自動擴展extendSetMaybe并且盡力復(fù)用既有實例的內(nèi)存以減少分配——這正是ReadFrom設(shè)計為方法而非構(gòu)造函數(shù)的原因。返回值是寫入/讀取的字節(jié)數(shù)。關(guān)于字節(jié)序與編碼包提供了全局配置函數(shù)BigEndian()/LittleEndian()/BinaryOrder()— 二進制序列化字節(jié)序默認(rèn)binary.BigEndianBase64StdEncoding()— 切換 JSON 編解碼的 base64 編碼方式默認(rèn)base64.URLEncoding這兩個開關(guān)分別由包級變量binaryOrder與base64Encoding控制bitset.go#L67-L71注意它們是包級全局狀態(tài)修改會影響包內(nèi)所有實例的序列化行為。除io.Writer/io.Reader流接口外位集還實現(xiàn)了標(biāo)準(zhǔn)接口encoding.BinaryMarshalerMarshalBinary/UnmarshalBinary與encoding/jsonMarshalJSON/UnmarshalJSONJSON 形式為 base64 字符串可直接用于json.Marshal與gob等場景。BinaryStorageSize()可預(yù)估二進制存儲所需的字節(jié)數(shù)。性能提示當(dāng)寫入/讀取目標(biāo)是文件或網(wǎng)絡(luò)連接時建議先用bufio包裝減少系統(tǒng)調(diào)用次數(shù)f, err : os.Create(myfile) w : bufio.NewWriter(f) f, err : os.Open(myfile) r : bufio.NewReader(f)內(nèi)存模型與壓縮位集的選擇內(nèi)存上需要牢記兩個約束N 位的位集至少占用 N/8 字節(jié)位集長度始終≥「已訪問的最大位下標(biāo) 1」——也就是說Set(131)一次就會觸發(fā)數(shù) GB 級的擴容。文檔明確警告it is possible to run out of memory while using a bitset。因此對「位稀疏」的大整數(shù)集合直接使用bitset可能并不劃算更合適的選擇是壓縮位圖 Roaring bitmap 及其 Go 實現(xiàn)RoaringBitmap/roaring。兩者可相互轉(zhuǎn)換mybitset : roaringbitmap.ToBitSet() // Roaring - 常規(guī)位集 newroaringbitmap : roaring.FromBitSet(mybitset) // 常規(guī)位集 - Roaringroaring庫以分段壓縮方式表達稀疏集合在保留集合運算能力的同時大幅降低稀疏場景的內(nèi)存占用。選型建議位域較密集或下標(biāo)范圍緊湊時用bitset直接獲得最大吞吐位域稀疏、跨度極大時用 Roaring需要兩者結(jié)合時通過上述 API 在運行時互轉(zhuǎn)。關(guān)于 Goroutine 安全文檔明確位集默認(rèn)不做任何同步跨 goroutine 并發(fā)訪問同一實例是不安全的they are unsynchronized for performance。如果確實需要多 goroutine 共享兩種官方建議通道傳遞所有權(quán)遵循 Go 慣例通過 channel 把*BitSet在 goroutine 間傳遞保證任意時刻只有一個持有者sync.Mutex串行化用互斥鎖包裹所有對位集的操作犧牲并發(fā)換取安全。從源碼看set []uint64的讀寫、extendSet的擴容均未加鎖因此任何形式的并發(fā)讀寫包括并發(fā)Test都可能造成數(shù)據(jù)競爭。需要頻繁共享時應(yīng)優(yōu)先考慮「每 goroutine 私有位集 周期性合并」的分治模式例如并行分段計算后InPlaceUnion匯總既規(guī)避鎖競爭又保留位集運算的高吞吐。測試與驗證原文檔要求提交前運行測試與覆蓋率檢查go test go test -cover本倉庫 vendor 目錄下的位集源碼vendor/github.com/bits-and-blooms/bitset包含bitset.go核心實現(xiàn)約 1800 行、bitset_iter.goGo 1.23 迭代器、select.goRank/Select 支持、popcnt.go種群計數(shù)以及pext.gen.go生成的位抽取指令封裝等文件配合倉庫根目錄 go.mod 中鎖定的v1.25.0版本即可復(fù)現(xiàn)文檔所述全部行為。位集相關(guān)功能在 Loki 中通常位于布隆過濾器、索引結(jié)構(gòu)等存儲路徑如 pkg/storage 下的 bloom/tsdb 相關(guān)實現(xiàn)可作為位集在真實大規(guī)模日志系統(tǒng)中的應(yīng)用參考。小結(jié)圍繞「非負(fù)整數(shù) ? 布爾值」這一核心抽象bitset提供了完整的方法矩陣單點操作的Set/Clear/Flip/Test/SetTo區(qū)間與全量的SetRange/FlipRange/SetAll/ClearAll集合層面的交集/并集/差集/補集/對稱差及其基數(shù)與原地變體迭代層面的NextSet/NextSetMany/PreviousSet/EachSet序列化層面的WriteTo/ReadFrom/MarshalBinary/MarshalJSON以及內(nèi)存管理層面的Shrink/Compact/Clone/Copy。將其內(nèi)化到自己的工具箱你可以在布隆過濾器、位圖索引、權(quán)限標(biāo)記、IP 分配、去重標(biāo)記等大量位密集型場景中以遠(yuǎn)低于map[uint]bool的內(nèi)存與時間成本完成集合建模與運算?!久赓M下載鏈接】lokiLike Prometheus, but for logs.項目地址: https://gitcode.com/GitHub_Trending/lok/loki創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考