Roaring bitmaps
Roaring bitmaps
最近看一篇文章,里面涉及到使用roaring bitmaps來推送用戶廣告并通過計算交集來降低用戶廣告推送次數。本文給出roaring bitmaps的原理和基本用法,后續給出原文的內容。
本文來自:A primer on Roaring bitmaps: what they are and how they work
目錄-
Roaring bitmaps
- 什么是bitmaps,bitmaps解決什么問題
- 什么是Roaring bitmaps
- Roaring bitmaps解決了哪些傳統bitmaps無法解決的問題?
-
Roaring bitmap是如何工作的
- Part 1: Roaring bitmaps 的內存布局
- Part 2: Roaring bitmaps中的集合操作
- Golang的roaring bitmaps
我從這篇解決大規模留存分析的文章中了解到了Roaring bitmaps,使用Roaring bitmaps而非傳統的bitmaps可以將應用使用的內存從~125G下降到300M,節省了99.8%的內存資源。
但這是如何做到的?
下面是兩篇與Roaring bitmaps相關的論文:
- This one proposes the data structure.
- This one introduces a critical optimization.
本文介紹了什么是bitmaps及其用途,什么是Roaring bitmaps以及它是如何解決傳統bitmaps中存在的問題的,并一步步揭示Roaring bitmaps的頂層機構及其工作方式。
bitmaps采用了許多算法、技術和啟發式方法,這里不作詳細介紹,這些細節對理解Roaring bitmaps的基本內部結構和操作并不重要。
什么是bitmaps,bitmaps解決什么問題
Bitmaps 是一個bits位數組,用于存儲整數集。
當集合中添加了一個整數N之后,會將第N個bit位設置為1,如下圖所示:
圖1:bitmaps的運作展示
通過這種存儲整數的方式,可以非常快速地使用CPU的位與和位或命令分別計算集合的交集和并集。
事實證明,對于很多查詢和數據庫應用來說,快速計算集合的交集和并集至關重要。查詢和數據庫索引中存在各種操作,這些操作可以歸結為需要快速計算出交集或并集的兩組整數集。
以反向查詢索引為例:
- 假設你已經為數十億個文檔設置了索引,且每個文檔都有一個整數id
-
index maps terms表示包含特定詞語的一組文檔。如
pigeon存在于id為{2, 345, 2034, ...}的一組文檔中。 - 使用集合操作來查詢多個terms。如為了計算出
carrier AND pigeon,你需要找出包含carrier的文檔集合和包含pigeon的文檔集合的交集。 - 使用位操作可以很快地進行集合操作。對于上述例子,只需要執行位與操作就可以找出表示文檔id的bit位。
但bitmaps在大規模整數集合場景下的壓縮效果不佳。
什么是Roaring bitmaps
roaringbitmap.org中有如下介紹:
Roaring bitmaps是一種壓縮的bitmaps,它比bitmaps快百倍。
Roaring bitmaps是一種優化的bitmaps,它和傳統的bitmaps一樣,都為整數提供了一種集合數據結構。可以插入整數,校驗整數的存在性,以及獲取兩個整數集合的交集和并集等。
相比傳統的bitmaps,Roaring bitmaps提供了更好的壓縮效果。更重要的是,采用這種方式并不會對性能造成顯著的影響。
roaringbitmap.org 中列舉了使用Roaring bitmaps 的OLAP數據庫和查詢系統。
Roaring bitmaps解決了哪些傳統bitmaps無法解決的問題?
對于一個稀疏集合,傳統的bitmaps的壓縮效果較差。
假設一個傳統bitmaps為空,添加一個整數8,000,000,此時:
- 首先分配1,000,000 字節的空間
- 然后將第8,000,000個bit位設置為1,如下圖所示:
圖2:如果在一個空的bitmaps中直接分配第800萬個bit位,會發生什么。
這種方式會出現如下問題:
- bitmaps中只設置了一個整數
- 而一個整數最多需要4個字節
- 但傳統的bitmaps卻使用了1M字節的內存,比所需的內存多了6個數量級。
Roaring bitmaps可以在解決該問題的同時保證集合操作的快速性。
先前的很多研究也試圖解決bitmaps壓縮性較差的問題,并取得了令人印象深刻的結果,但代價是集合操作的性能。
Roaring bitmap是如何工作的
Roaring bitmap使用了多種方式來改善傳統bitmaps的性能。
Part 1: Roaring bitmaps 的內存布局
所有32位整數都被劃分為連續的塊(chunk)。
圖3:如何在Roaring bitmap中將32位的整數空間劃分為chunk
Roaring bitmaps最多可以支持2^16個chunks,每個chunk共享相同的16個最高有效位(Msb),
如上圖所示,Roaring bitmaps使用的分區方案可以確保一個整數始終屬于2^16(或65536)個連續整數所在的某個chunk。
注意:此外還有64位的Roaring bitmaps實現,本文不對此做深入討論。
chunks是Roaring bitmaps中對整數的邏輯劃分。屬于一個chunk的所有整數在物理上都保存在相同的container中。
圖4:來自第一篇Roaring bitmap 論文中的3個containers的例子。
cardinality表示元素個數。
上圖展示了3個不同的chunks,對應的3個不同的containers。一個chunk能且只能對應Roaring bitmap中的一個container。
如果將62的倍數的前1000個元素插入到Roaring位圖中,那么它們將最終位于圖4最左邊的容器中。這個容器的cardinality為1000。如果后續插入了整數63,則會落入相同的container中,容器的cardinality將是1001。
后續可以看到,container的cardinality決定了它在內存中的表達方式。
稀疏containers:包含<=4096個整數,它們存儲為有序的壓縮數組。
圖4中最左和中間的兩個containers(cardinalities為1000和100)是稀疏的,因此它們將被存儲為16位整數的有序壓縮數組。
通過壓縮,可以將32位稀疏壓縮為16位整數,見下圖:
圖5:圖2中的兩個稀疏Roaring bitmap container,以及它們如何在內存中存儲的示例。
每個container最多可以保存2^16個不同的整數。為了從稀疏container中獲取原始的32位整數,可以將16位整數和container的高16位組合起來獲取原始整數。
這些數組是動態分配的,因此一個稀疏container中的內存會隨著整數的累計而增加。
密集容器:包含>4096個整數,它們被存儲為bitmaps。
圖4中最右邊的container為密集型container(cardinality 為2^15),因此它會被存儲為傳統的bitmaps。
密集containers為bitmaps,包含2^16位(8KB)的bitmaps,直接分配存儲。bitmaps中的第N個bit位對應chunk中的第N個整數。
一級索引指向所有容器,索引存儲為有序數組。
一級索引中存儲了Roaring bitmap中每個container的高16位,以及指向對應container的指針。
圖7:一級索引中指向圖2、3和4中描述的containern的指針
索引存儲為有序數組,并隨著Roaring bitmap中containers的增加而動態增長。
Part 2: Roaring bitmaps中的集合操作
整數的插入會因container類型而異,可能會導致container的類型發生變化。
為了插入整數N,首先獲取N的高16位(N/2^16),并在Roaring bitmap中找到N對應的container。
Array container和bitmap container的插入操作不同:
- Bitmap container:將第
N % 2^16個bit位設置為1。注意bitmap是直接分配的。 - Array container:在有序數組的第
N % 2^16個位置插入N。注意數組是動態分配的,隨數據的增加而增加。
插入操作可能會改變container的類型,例如一個Array container中有4096個整數,則插入操作會將其轉換為一個bitmap container,然后將第N % 2^16個bit位設置為1。
如果一個container不存在,則會首先創建一個新的Array container,然后將其加入Roaring bitmap的一級索引中,最后將N添加到Array container中。
校驗數值的存在性會隨container類型而異
為了校驗是否存在整數N,首先獲取N的高16位(N % 2^16),然后用它在Roaring bitmap中找到對應的container。
如果container不存在,則N也不存在。
Array container和bitmap container的存在性校驗方式不同:
- Bitmap container:校驗第
N % 2^16個bit位是否為1 - Array container:使用二分法在有序數組中找到第
N % 2^16個位置的值
計算兩個Roaring bitmaps的交集。算法會因container類型而異,且container類型也可能發生變化。
為了計算Roaring bitmaps A和B的交集,只需要計算A和B中匹配的containers的交集即可。匹配的container為兩個Roaring bitmaps中高16位相同的container,即相同的chunk。
交集運算會隨container的類型而異,分為:
- Bitmap / Bitmap: 計算兩個Bitmaps的位與即可。如果cardinality<=4096,則將結果保存在Array container中,否則保存在bitmap container中。
- Bitmap / Array: 遍歷數組,然后在bitmap中校驗每個16位整數的存在性。如果整數存在,則將其添加到一個Array container中。注意Bitmap和array container的交集總是會創建出一個array container。
- Array / Array: 兩個array containers的交集總是會生成一個新的array container。交集的運算性能會隨著cardinality變化(此篇論文的第5頁底部有描述),可以是簡單的合并(和merge sort的方式相同)或快速交集(參見該論文)。
如果一個Roaring bitmap中的某個container沒有對應的container,則不會出現在結果中,即交集為空。
Roaring bitmap 的并集。算法會隨container類型而異,container類型也可能變化
為了計算Roaring bitmaps A和B的并集。需要計算A和B中匹配containers的并集。
并集運算可能會因container類型而異,有如下幾種:
- Bitmap / Bitmap: 計算兩個bitmaps的位或。兩個bitmap container的并集總是會創建另一個bitmap container。
- Bitmap / Array: 復制bitmap,并在該bitmap中為array container中的所有整數設置bit位。bitmap和array container的并集總是會創建另一個bitmap container。
- Array / Array: 如果兩個array container的cardinalities總數<=4096,則生成的container會是一個array container。這種情況下,會將兩個arrays中的所有整數添加到一個新的array container中。否則會假設生成的container是一個bitmap:創建一個新的bitmap container,然后在該bitmap中為兩個array containers中的整數設置bit位。如果生成的container的cardinality<=4096,則將該bitmap container轉換為一個array container。
最后,將A和B中沒有匹配container的所有containers添加到結果中。
Part 3:第三種也是最后一種container類型——"run" container——如何優化大量連續的整數
part 1和2中涵蓋了Roaring bitmaps的大分部內部結構和操作。最后討論一下Roaring bitmaps的第二篇論文中的一個重要優化。
run container為使用兩個16位整數表示的連續整數:run開始和run長度。
第二篇論文的第3頁有如下表述:
新容器在概念上很簡單:給定一個run(例如[10,1000]),我們存儲起點(10)及其長度減1(990)。然后將起點和長度成對打包,開始值和長度值都為16位整數。
這種技術稱為run-length編碼。Run-length可以有效壓縮bitmaps,但在很多場景下,卻降低了set操作的性能。
當客戶端調用runOptimize函數時,run container是顯式形成的,而在某些情況下,當向Roaring bitmap中添加了大范圍數值時,則是隱式形成的。
與稀疏和密集container不同,run container通常不會自動形成。
- 客戶端可以調用runOptimize來優化Roaring bitmap中的大量連續整數,這種情況下,run container可能會替代現有的array 或 bitmap container。
- Roaring bitmap提供了一個添加連續數值的操作,這種情況下,可能會形成run container。
該篇論文沒有具體規定如何以及合時會發生第二種場景。可能場景是,為一個還沒有container的chunk添加了一段連續的值,那么此時創建一個run container(而不是array或bitmap container)可能更有意義。
runOptimize僅在run container小于要替換的container時才會創建該container。
runOptimize首先會計算一個container中的連續值的數量。然后再決定是否需要創建一個run container:run container必須要小于等同的array或bitmap container。
在第2篇論文的第6,7頁描述了一種用于計算連續值數量的算法:
run container的添加為所有集合操作引入了新的算法。
Roaring bitmaps論文中并沒有描述run container的插入和校驗整數存在性的算法:這些操作相對簡單。
但是,添加run container需要為如下組合實現高性能并集和交集算法:
- Run / Run
- Run / Array
- Run / Bitmap
這里不再作深入討論,這些算法也不會太復雜(參見該論文的第10頁)。
Roaring bitmaps使用了多種算法和技術,與其他bitmaps實現相比,可以實現更好的壓縮效果和更快的性能。
Roaring bitmaps的實現很有挑戰性,但它的表現卻很好,尤其是在OLAP工作負載中使用時。創建者設法根除常見的多種場景中存在的低效率問題——稀疏數據、密集數據、大量連續的數據——并且同時解決了所有這些問題。
第3篇論文描述了創建者使用C語言編寫的一個實現,該實現利用了他們使用SIMD(單指令多數據)指令設計的矢量化算法。這里提供了該實現、CRoaring以及其他多種語言的實現。它們被用于主流的柱狀數據庫和搜索應用程序,并得到了積極的維護、改進和優化。
Golang的roaring bitmaps
Roaring bitmaps可以實現整數集合的交集和并集運算,并在保證數據壓縮效果的同時同時保證了運算的高效性。
這里給出了golang版本的實現。分為32位和64位兩種。需要注意的是bitmaps并不是goroutines安全的。下面32位的Roaring bitmaps為例看下bitmap container和array container是如何添加數據的。
在上文中有講,當container為bitmaps類型時,會直接分配存儲,從下面bitmap container的初始化中可以看到,其初始化會直接分配65535 bit位的存儲空間。當bitmap存儲滿后,會被壓縮為run container。
func newBitmapContainer() *bitmapContainer {
p := new(bitmapContainer)
size := (1 << 16) / 64
p.bitmap = make([]uint64, size, size)
return p
}
而array container中主要用于存儲稀疏數值。下面是在array container中添加數值的函數。可以看到array container并不是預先分配的,它隨添加的數值的增加而增加。
func (ac *arrayContainer) iaddReturnMinimized(x uint16) container {
// Special case adding to the end of the container.
l := len(ac.content)
// arrayDefaultMaxSize為4096。下面表示如果當前container中的數值總數沒有超過最大值,
// 且要添加的值x大于有序數組的最后一個時,只需要將x追加到有序數組的最后一個即可
if l > 0 && l < arrayDefaultMaxSize && ac.content[l-1] < x {
ac.content = append(ac.content, x)
return ac
}
// 使用二分法找到x或插入x的位置
loc := binarySearch(ac.content, x)
// 如果loc<0表示沒有在container中找到x,如果當前container中的數值總數為arrayDefaultMaxSize,
// 則需要轉換為bitmap container,然后再添加x。
// 否則根據找到的位置loc,再在array container中插入x
if loc < 0 {
if len(ac.content) >= arrayDefaultMaxSize {
a := ac.toBitmapContainer()
a.iadd(x)
return a
}
s := ac.content
i := -loc - 1
s = append(s, 0)
copy(s[i+1:], s[i:])
s[i] = x
ac.content = s
}
return ac
}
總結
以上是生活随笔為你收集整理的Roaring bitmaps的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 论文根据研究内容分为几个类型
- 下一篇: 巴蜀之画研究论文目的