高效壓縮位圖RoaringBitmap的原理與應(yīng)用

目錄

位圖法簡(jiǎn)述

對(duì)于我們大數(shù)據(jù)工作者來(lái)說(shuō),海量數(shù)據(jù)的判重和基數(shù)統(tǒng)計(jì)是兩個(gè)繞不開(kāi)的基礎(chǔ)問(wèn)題。之前我已經(jīng)講了兩種應(yīng)用廣泛的方法,即布隆過(guò)濾器HyperLogLog。雖然它們節(jié)省空間并且效率高,但也付出了一定的代價(jià),即:

  • 只能插入元素,不能刪除元素;
  • 不保證100%準(zhǔn)確,總是存在誤差。

這兩個(gè)缺點(diǎn)可以說(shuō)是所有概率性數(shù)據(jù)結(jié)構(gòu)(probabilistic data structure)做出的trade-off,畢竟魚(yú)與熊掌不可兼得嘛。

話說(shuō)回來(lái),有什么相對(duì)高效的能夠保證絕對(duì)精確的方法呢?最樸素的思路是利用布隆過(guò)濾器和HyperLogLog的基礎(chǔ)——位數(shù)組,也叫位圖(bitmap)。不妨來(lái)看一道老生常談的面試題:

給定含有40億個(gè)不重復(fù)的位于[0, 232 - 1]區(qū)間內(nèi)的整數(shù)的集合,如何快速判定某個(gè)數(shù)是否在該集合內(nèi)?

顯然,如果我們將這40億個(gè)數(shù)原樣存儲(chǔ)下來(lái),需要耗費(fèi)高達(dá)14.9GB的內(nèi)存,不可接受。所以我們可以用位圖來(lái)存儲(chǔ),即第0個(gè)比特表示數(shù)字0,第1個(gè)比特表示數(shù)字1,以此類推。如果某個(gè)數(shù)位于原集合內(nèi),就將它對(duì)應(yīng)的位圖內(nèi)的比特置為1,否則保持為0。這樣就能很方便地查詢得出結(jié)果了,僅僅需要占用512MB的內(nèi)存,只有原來(lái)的不到3.4%。

由于位圖的這個(gè)特性,它經(jīng)常被作為索引用在數(shù)據(jù)庫(kù)、查詢引擎和搜索引擎中,并且位操作(如and求交集、or求并集)之間可以并行,效率更好。但是,位圖也不是完美無(wú)缺的:不管業(yè)務(wù)中實(shí)際的元素基數(shù)有多少,它占用的內(nèi)存空間都恒定不變。舉個(gè)例子,如果上文題目中的集合只存儲(chǔ)了0這一個(gè)元素,那么該位圖只有最低位是1,其他位全為0,但仍然占用了512MB內(nèi)存。數(shù)據(jù)越稀疏,空間浪費(fèi)越嚴(yán)重。

為了解決位圖不適應(yīng)稀疏存儲(chǔ)的問(wèn)題,大佬們提出了多種算法對(duì)稀疏位圖進(jìn)行壓縮,減少內(nèi)存占用并提高效率。比較有代表性的有WAH、EWAH、Concise,以及RoaringBitmap。前三種算法都是基于行程長(zhǎng)度編碼(Run-length encoding, RLE)做壓縮的,而RoaringBitmap算是它們的改進(jìn)版,更加優(yōu)秀,因此本文重點(diǎn)探討它。

RoaringBitmap的思路

為了不用打那么多字,下文將RoaringBitmap簡(jiǎn)稱為RBM。

RBM的歷史并不長(zhǎng),它于2016年由S. Chambi、D. Lemire、O. Kaser等人在論文《Better bitmap performance with Roaring bitmaps》《Consistently faster and smaller compressed bitmaps with Roaring》中提出,官網(wǎng)在這里

RBM的主要思路是:將32位無(wú)符號(hào)整數(shù)按照高16位分桶,即最多可能有216=65536個(gè)桶,論文內(nèi)稱為container。存儲(chǔ)數(shù)據(jù)時(shí),按照數(shù)據(jù)的高16位找到container(找不到就會(huì)新建一個(gè)),再將低16位放入container中。也就是說(shuō),一個(gè)RBM就是很多container的集合。

為了方便理解,照搬論文中的示例圖,如下所示。

圖中示出了三個(gè)container:

  • 高16位為0000H的container,存儲(chǔ)有前1000個(gè)62的倍數(shù)。
  • 高16位為0001H的container,存儲(chǔ)有[216, 216+100)區(qū)間內(nèi)的100個(gè)數(shù)。
  • 高16位為0002H的container,存儲(chǔ)有[2×216, 3×216)區(qū)間內(nèi)的所有偶數(shù),共215個(gè)。

container是RBM新創(chuàng)造的概念,自然也是提高效率的核心。為了更高效地存儲(chǔ)和查詢數(shù)據(jù),不同情況下會(huì)采用不同類型的container,下面深入講解一下container的細(xì)節(jié)。

Container原理

一共有3種。

ArrayContainer

當(dāng)桶內(nèi)數(shù)據(jù)的基數(shù)不大于4096時(shí),會(huì)采用它來(lái)存儲(chǔ),其本質(zhì)上是一個(gè)unsigned short類型的有序數(shù)組。數(shù)組初始長(zhǎng)度為4,隨著數(shù)據(jù)的增多會(huì)自動(dòng)擴(kuò)容(但最大長(zhǎng)度就是4096)。另外還維護(hù)有一個(gè)計(jì)數(shù)器,用來(lái)實(shí)時(shí)記錄基數(shù)。

上圖中的前兩個(gè)container基數(shù)都沒(méi)超過(guò)4096,所以均為ArrayContainer。

BitmapContainer

當(dāng)桶內(nèi)數(shù)據(jù)的基數(shù)大于4096時(shí),會(huì)采用它來(lái)存儲(chǔ),其本質(zhì)就是上一節(jié)講過(guò)的普通位圖,用長(zhǎng)度固定為1024的unsigned long型數(shù)組表示,亦即位圖的大小固定為216位(8KB)。它同樣有一個(gè)計(jì)數(shù)器。

上圖中的第三個(gè)container基數(shù)遠(yuǎn)遠(yuǎn)大于4096,所以要用BitmapContainer存儲(chǔ)。

RunContainer

RunContainer在圖中并未示出,初始的RBM實(shí)現(xiàn)中也沒(méi)有它,而是在本節(jié)開(kāi)頭的第二篇論文中新加入的。它使用可變長(zhǎng)度的unsigned short數(shù)組存儲(chǔ)用行程長(zhǎng)度編碼(RLE)壓縮后的數(shù)據(jù)。舉個(gè)例子,連續(xù)的整數(shù)序列11, 12, 13, 14, 15, 27, 28, 29會(huì)被RLE壓縮為兩個(gè)二元組11, 4, 27, 2,表示11后面緊跟著4個(gè)連續(xù)遞增的值,27后面跟著2個(gè)連續(xù)遞增的值。

由此可見(jiàn),RunContainer的壓縮效果可好可壞。考慮極端情況:如果所有數(shù)據(jù)都是連續(xù)的,那么最終只需要4字節(jié);如果所有數(shù)據(jù)都不連續(xù)(比如全是奇數(shù)或全是偶數(shù)),那么不僅不會(huì)壓縮,還會(huì)膨脹成原來(lái)的兩倍大。所以,RBM引入RunContainer是作為其他兩種container的折衷方案。

下面來(lái)簡(jiǎn)要看看它們的復(fù)雜度和轉(zhuǎn)換方法。

時(shí)空分析

增刪改查的時(shí)間復(fù)雜度方面,BitmapContainer只涉及到位運(yùn)算,顯然為O(1)。而ArrayContainer和RunContainer都需要用二分查找在有序數(shù)組中定位元素,故為O(logN)。

空間占用(即序列化時(shí)寫(xiě)出的字節(jié)流長(zhǎng)度)方面,BitmapContainer是恒定為8192B的。ArrayContainer的空間占用與基數(shù)(c)有關(guān),為(2 + 2c)B;RunContainer的則與它存儲(chǔ)的連續(xù)序列數(shù)(r)有關(guān),為(2 + 4r)B。以上節(jié)圖中的RBM為例,它一共存儲(chǔ)了33868個(gè)unsigned int,只占用了10396個(gè)字節(jié)的空間,可以說(shuō)是非常高效了。

Container的創(chuàng)建與轉(zhuǎn)換

在創(chuàng)建一個(gè)新container時(shí),如果只插入一個(gè)元素,RBM默認(rèn)會(huì)用ArrayContainer來(lái)存儲(chǔ)。如果插入的是元素序列的話,則會(huì)先根據(jù)上面的方法計(jì)算ArrayContainer和RunContainer的空間占用大小,并選擇較小的那一種進(jìn)行存儲(chǔ)。

當(dāng)ArrayContainer的容量超過(guò)4096后,會(huì)自動(dòng)轉(zhuǎn)成BitmapContainer存儲(chǔ)。4096這個(gè)閾值很聰明,低于它時(shí)ArrayContainer比較省空間,高于它時(shí)BitmapContainer比較省空間。也就是說(shuō)ArrayContainer存儲(chǔ)稀疏數(shù)據(jù),BitmapContainer存儲(chǔ)稠密數(shù)據(jù),可以最大限度地避免內(nèi)存浪費(fèi)。

RBM還可以通過(guò)調(diào)用特定的API(名為optimize)比較ArrayContainer/BitmapContainer與等價(jià)的RunContainer的內(nèi)存占用情況,一旦RunContainer占用較小,就轉(zhuǎn)換之。也就是說(shuō),上圖例子中的第二個(gè)ArrayContainer可以轉(zhuǎn)化為只有一個(gè)二元組0, 100的RunContainer,占用空間進(jìn)一步下降到10200字節(jié)。

RBM的應(yīng)用

官方提供了RBM的多種語(yǔ)言實(shí)現(xiàn),Java、C/C++、Python、Go、C#等等一應(yīng)俱全。Java版本的GitHub repo見(jiàn)這里。代碼比較多,但思路很清晰,看官如果對(duì)位運(yùn)算比較熟悉的話讀起來(lái)不難,故本文就不再長(zhǎng)篇大論地講源碼了。值得注意的幾點(diǎn)如下:

  • 兩個(gè)RBM做集合操作時(shí),不同種類container之間位運(yùn)算的處理方式,如ArrayContainer AND BitmapContainer,BitmapContainer OR RunContainer等;
  • 對(duì)64位整數(shù)的支持(32位有時(shí)會(huì)不夠用哈);
  • 能夠?qū)BM數(shù)據(jù)寫(xiě)到堆外,即內(nèi)存映射;
  • 支持Kryo序列化方式。

RBM的應(yīng)用范圍極廣,下面只簡(jiǎn)單列舉幾個(gè)有代表性的應(yīng)用,并給出reference。

Lucene

為了加速搜索,Lucene會(huì)將常用的查詢過(guò)濾條件產(chǎn)生的結(jié)果集緩存到內(nèi)存中,方便復(fù)用,稱為filter cache。結(jié)果集其實(shí)就是文檔ID(整形數(shù))的集合。從Lucene 5開(kāi)始,使用了RBM優(yōu)化過(guò)的文檔ID集合RoaringDocIdSet作為filter cache,詳情可以參見(jiàn)《Frame of Reference and Roaring Bitmaps》。該文除了介紹RBM外,還介紹了壓縮倒排索引的Frame of Reference(FOR)編碼,值得一讀。

Spark

在Spark Core的MapStatus組件(用來(lái)跟蹤ShuffleMapTask的輸出結(jié)果塊)中,利用了RBM來(lái)存儲(chǔ)塊是否非空的狀態(tài)。今后會(huì)在Spark連載里講到它,所以現(xiàn)在看看該類的源碼就可以了,不難理解。

Greenplum

我司是Greenplum大戶,雖然本鶸現(xiàn)在不負(fù)責(zé)數(shù)倉(cāng)相關(guān)的事情了,但是偶爾還是要向GP提供一些數(shù)據(jù)。GP配合RoaringBitmap非常適合做海量用戶的近實(shí)時(shí)畫(huà)像,每個(gè)RBM代表一維標(biāo)簽即可,根據(jù)標(biāo)簽圈選用戶也很方便。GP原生并未支持RBM類型數(shù)據(jù),需要安裝一個(gè)擴(kuò)展插件,見(jiàn)這里。關(guān)于GP與RBM的整合與使用,有兩篇不錯(cuò)的參考文章:

Redis

我們?cè)赗edis里經(jīng)常使用位圖存儲(chǔ)數(shù)據(jù)(Redis原生以字符串的形式支持位圖),當(dāng)然也就會(huì)遇到稀疏位圖浪費(fèi)存儲(chǔ)空間的問(wèn)題。但要讓Redis支持RBM,需要引入專門(mén)的module,項(xiàng)目地址見(jiàn)這里。它的設(shè)計(jì)思想與Java版RBM幾乎相同,不再?gòu)U話了。

The End

晚安咯。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容