<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
集合是軟體中的基本抽象。實現集合的方法有很多,例如 hash set、tree等。要實現一個整數集合,點陣圖(bitmap,也稱為 bitset 位集合,bitvector 位向量)是個不錯的方法。使用 n 個位(bit),我們可以表示整數範圍[0, n)
。如果整數 i 在集合中,第 i 位設定為 1。這樣集合的交集(intersection)、並集(unions)和差集(difference)可以利用整數的按位元與、按位元或和按位元與非來實現。而計算機執行位運算是非常迅速的。
上一篇文章我介紹了bitset這個庫。
bitset 在某些場景中會消耗大量的記憶體。例如,設定第 1,000,000 位,需要佔用超過 100kb 的記憶體。為此 bitset 庫的作者又開發了壓縮點陣相簿:roaring。
本文首先介紹了 roaring 的使用。最後分析 roaring 的檔案儲存格式。
本文程式碼使用 Go Modules。
建立目錄並初始化:
$ mkdir -p roaring && cd roaring $ go mod init github.com/darjun/go-daily-lib/roaring
安裝roaring
庫:
$ go get -u github.com/RoaringBitmap/roaring
func main() { bm1 := roaring.BitmapOf(1, 2, 3, 4, 5, 100, 1000) fmt.Println(bm1.String()) // {1,2,3,4,5,100,1000} fmt.Println(bm1.GetCardinality()) // 7 fmt.Println(bm1.Contains(3)) // true bm2 := roaring.BitmapOf(1, 100, 500) fmt.Println(bm2.String()) // {1,100,500} fmt.Println(bm2.GetCardinality()) // 3 fmt.Println(bm2.Contains(300)) // false bm3 := roaring.New() bm3.Add(1) bm3.Add(11) bm3.Add(111) fmt.Println(bm3.String()) // {1,11,111} fmt.Println(bm3.GetCardinality()) // 3 fmt.Println(bm3.Contains(11)) // true bm1.Or(bm2) // 執行並集 fmt.Println(bm1.String()) // {1,2,3,4,5,100,500,1000} fmt.Println(bm1.GetCardinality()) // 8 fmt.Println(bm1.Contains(500)) // true bm2.And(bm3) // 執行交集 fmt.Println(bm2.String()) // {1} fmt.Println(bm2.GetCardinality()) // 1 fmt.Println(bm2.Contains(1)) // true }
上面演示了兩種建立 roaring bitmap 的方式:
roaring.BitmapOf()
:傳入集合元素,建立點陣圖並新增這些元素roaring.New()
:建立一個空點陣圖首先,我們建立了一個點陣圖 bm1:{1,2,3,4,5,100,1000}。輸出它的字串表示,集合大小,檢查 3 是否在集合中。
然後又建立了一個點陣圖 bm2:{1,100,500}。輸出檢查三連。
接著建立了一個空點陣圖 bm3,依次新增元素 1,11,111。輸出檢查三連。
然後我們對 bm1 和 bm2 執行並集,結果直接存放在 bm1 中。由於集合中的元素各不相同,此時 bm1 中的元素為{1,2,3,4,5,100,500,1000},大小為 8。
再然後我們對 bm2 和 bm3 執行交集,結果直接存放在 bm2 中。此時 bm2 中的元素為{1},大小為 1。
可以看出 roaring 提供的基本操作與 bitset 大體相同。只是命名完全不一樣,在使用時需要特別注意。
bm.String()
:返回 bitmap 的字串表示bm.Add(n)
:新增元素 nbm.GetCardinality()
:返回集合的基數(Cardinality),即元素個數bm1.And(bm2)
:執行集合交集,會修改 bm1bm1.Or(bm2)
:執行集合並集,會修改 bm1roaring 點陣圖支援迭代。
func main() { bm := roaring.BitmapOf(1, 2, 3, 4, 5, 100, 1000) i := bm.Iterator() for i.HasNext() { fmt.Println(i.Next()) } }
與很多程式語言支援的迭代器一樣,先呼叫物件的Iterator()
返回一個迭代器,然後迴圈呼叫HasNext()
檢查是否有下一個元素,呼叫i.Next()
返回下一個元素。
上面程式碼依次輸出 1,2,3,4,5,100,1000。
roaring 支援點陣圖集合運算的並行執行。可以指定使用多少個 goroutine 對集合執行交集、並集等。同時可以傳入可變數量的點陣圖集合:
func main() { bm1 := roaring.BitmapOf(1, 2, 3, 4, 5, 100, 1000) bm2 := roaring.BitmapOf(1, 100, 500) bm3 := roaring.BitmapOf(1, 10, 1000) bmAnd := roaring.ParAnd(4, bm1, bm2, bm3) fmt.Println(bmAnd.String()) // {1} fmt.Println(bmAnd.GetCardinality()) // 1 fmt.Println(bmAnd.Contains(1)) // true fmt.Println(bmAnd.Contains(100)) // false bmOr := roaring.ParOr(4, bm1, bm2, bm3) fmt.Println(bmOr.String()) // {1,2,3,4,5,10,100,500,1000} fmt.Println(bmOr.GetCardinality()) // 9 fmt.Println(bmOr.Contains(10)) // true }
並行操作使用相應介面的Par*
版本,第一個引數指定 worker 數量,接著傳入任意多個 bitmap。
roaring 可以將壓縮的點陣圖寫入到檔案中,並且格式與其他語言的實現保持相容。也就是說,我們可以用 Go 將 roaring 點陣圖寫入檔案,然後通過網路傳送給另一臺機器,在這臺機器上使用 C++ 或 Java 的實現讀取這個檔案。
func main() { bm := roaring.BitmapOf(1, 3, 5, 7, 100, 300, 500, 700) buf := &bytes.Buffer{} bm.WriteTo(buf) newBm := roaring.New() newBm.ReadFrom(buf) if bm.Equals(newBm) { fmt.Println("write and read back ok.") } }
WriteTo(w io.Writer)
:寫入一個 io.Writer,可以是記憶體(byte.Buffer),可以是檔案(os.File),甚至可以是網路(net.Conn)ReadFrom(r io.Reader)
:從一個 io.Reader 中讀取,來源同樣可以是記憶體、檔案或網路等注意WriteTo
的返回值為size
和err
,使用時需要處理錯誤情況。ReadFrom
也是返回size
和err
,同樣需要處理處理。
預設情況下,roaring 點陣圖只能用來儲存 32 位整數。所以 roaring 點陣圖最多能包含 4294967296(2^32
) 個整數。
roaring 也提供了儲存 64 位整數的擴充套件,即github.com/RoaringBitmap/roaring/roaring64
。提供的介面基本相同。然而,64 位版本不保證與 Java/C++ 等格式相容。
roaring 可以寫入檔案中,也可以從檔案中讀取。並且提供多種語言相容的格式。下面我們一起來看看儲存的格式。
roaring 點陣圖預設只能儲存 32 位的整數。在序列化時,將這些整數分容器(container)儲存。每個容器有一個 16 位表示的基數(Cardinality,即元素個數,範圍[1,2^16]
)和一個鍵(key)。鍵取元素的最高有效 16 位(most significant),所以鍵的範圍為[0, 65536)
。這樣如果兩個整數的最高 16 位有效位相同,那麼它們將被儲存在同一個容器中。這樣做還有一個好處:可以減少佔用的空間。
所有整數均採用小端儲存。
roaring 採用的儲存格式佈局如下:
從上到下依次介紹。
開始部分是一個 Cookie Header。它用來識別一個二進位制流是不是一個 roaring 點陣圖,並且儲存一些少量資訊。
cookie 這個詞有點意思,本意是餅乾。我的理解是指小物件,所以 http 中的 cookie 只是用來儲存少量資訊。這裡的 Cookie Header 也是如此。
接下來是 Descriptive Header。見名知義,它用來描述容器的資訊。後面會詳細介紹容器。
接下來有一個可選的 Offset Header。它記錄了每個容器相對於首位的偏移,這讓我們可以隨機存取任意容器。
最後一部分是儲存實際資料的容器。roaring 中一共有 3 種型別的容器:
設計這種的佈局,是為了不用將儲存的點陣圖全部載入記憶體就可以隨機讀取它的資料。並且每個容器的範圍相互獨立,這使得平行計算變得容易。
Cookier Header 有兩種型別,分別佔用 32bit 和 64bit 的空間。
第一種型別,前 32bit 的值為 12346,此時緊接著的 32bit 表示容器數量(記為 n)。同時這意味著,後面沒有 run 型別的容器。12346 這魔術數位被定義為常數SERIAL_COOKIE_NO_RUNCONTAINER
,含義不言自明。
第二種型別,前 32bit 的最低有效 16 位的值為 12347。此時,最高有效 16 位儲存的值等於容器數量-1。將 cookie 右移 16 位再加 1 即可得到容器數量。由於這種型別的容器數量不會為 0,採用這種編碼我們能的容器數量會多上 1 個。這種方法在很多地方都有應用,例如 redis。後面緊接著會使用 (n+7)/8
位元組(作為一個 bitset)表示後面的容器是否 run 容器。每位對應一個容器,1 表示對應的容器是 run 容器,0 表示不是 run 容器。
由於是小端儲存,所以流的前 16bit 一定是 12346 或 12347。如果讀取到了其它的值,說明檔案損壞,直接退出程式即可。
Cookie Header 之後就是 Descriptive Header。它使用一對 16bit 資料描述每個容器。一個 16bit 儲存鍵(即整數的最高有效 16bit),另一個 16bit 儲存對應容器的基數(Cardinality)-1(又見到了),即容器儲存的整數數量)。如果有 n 個容器,則 Descriptive Header 需要 32n 位 或 4n 位元組。
掃描 Descriptive Header 之後,我們就能知道每個容器的型別。如果 cookie 值為 12347,cookie 後有一個 bitset 表示每個容器是否是 run 型別。對於非 run 型別的容器,如果容器的基數(Cardinality)小於等於 4096,它是一個 array 容器。反之,這是一個 bitset 容器
滿足以下任一條件,Offset Header 就會存在:
NO_OFFSET_THRESHOLD = 4
Offset Header 為每個容器使用 32bit 值儲存對應容器距離流開始處的偏移,單位位元組。
接下來就是實際儲存資料的容器了。前面簡單提到過,容器有三種型別。
儲存有序的 16bit 無符號整數值,有序便於使用二分查詢提高效率。16bit 值只是資料的最低有效 16bit,還記得 Descriptive Header 中每個容器都有一個 16bit 的 key 吧。將它們拼接起來才是實際的資料。
如果容器有 x 個值,佔用空間 2x 位元組。
bitset 容器固定使用 8KB 的空間,以 64bit 為單位(稱為字,word)序列化。因此,如果值 j 存在,則第 j/64 個字(從 0 開始)的 j%64 位會被設定為 1(從 0 開始)。
以一個表示 run 數量的 16bit 整數開始。後續每個 run 用一對 16bit 整數表示,前一個 16bit 表示開始的值,後一個 16bit 表示長度-1(又雙見到了)。例如,11,4 表示資料 11,12,13,14,15。
驗證我們是否真的理解了 roaring 佈局最有效的方法就是手擼一個解析。使用標準庫encoding/binary
可以很容易地處理大小端問題。
定義常數:
const ( SERIAL_COOKIE_NO_RUNCONTAINER = 12346 SERIAL_COOKIE = 12347 NO_OFFSET_THRESHOLD = 4 )
讀取 Cookie Header:
func readCookieHeader(r io.Reader) (cookie uint16, containerNum uint32, runFlagBitset []byte) { binary.Read(r, binary.LittleEndian, &cookie) switch cookie { case SERIAL_COOKIE_NO_RUNCONTAINER: var dummy uint16 binary.Read(r, binary.LittleEndian, &dummy) binary.Read(r, binary.LittleEndian, &containerNum) case SERIAL_COOKIE: var u16 uint16 binary.Read(r, binary.LittleEndian, &u16) containerNum = uint32(u16) buf := make([]uint8, (containerNum+7)/8) r.Read(buf) runFlagBitset = buf[:] default: log.Fatal("unknown cookie") } fmt.Println(cookie, containerNum, runFlagBitset) return }
讀取 Descriptive Header:
func readDescriptiveHeader(r io.Reader, containerNum uint32) []KeyCard { var keycards []KeyCard var key uint16 var card uint16 for i := 0; i < int(containerNum); i++ { binary.Read(r, binary.LittleEndian, &key) binary.Read(r, binary.LittleEndian, &card) card += 1 fmt.Println("container", i, "key", key, "card", card) keycards = append(keycards, KeyCard{key, card}) } return keycards }
讀取 Offset Header:
func readOffsetHeader(r io.Reader, cookie uint16, containerNum uint32) { if cookie == SERIAL_COOKIE_NO_RUNCONTAINER || (cookie == SERIAL_COOKIE && containerNum >= NO_OFFSET_THRESHOLD) { // have offset header var offset uint32 for i := 0; i < int(containerNum); i++ { binary.Read(r, binary.LittleEndian, &offset) fmt.Println("offset", i, offset) } } }
讀取容器,根據型別呼叫不同的函數:
// array func readArrayContainer(r io.Reader, key, card uint16, bm *roaring.Bitmap) { var value uint16 for i := 0; i < int(card); i++ { binary.Read(r, binary.LittleEndian, &value) bm.Add(uint32(key)<<16 | uint32(value)) } } // bitmap func readBitmapContainer(r io.Reader, key, card uint16, bm *roaring.Bitmap) { var u64s [1024]uint64 for i := 0; i < 1024; i++ { binary.Read(r, binary.LittleEndian, &u64s[i]) } bs := bitset.From(u64s[:]) for i := uint32(0); i < 8192; i++ { if bs.Test(uint(i)) { bm.Add(uint32(key)<<16 | i) } } } // run func readRunContainer(r io.Reader, key uint16, bm *roaring.Bitmap) { var runNum uint16 binary.Read(r, binary.LittleEndian, &runNum) var startNum uint16 var length uint16 for i := 0; i < int(runNum); i++ { binary.Read(r, binary.LittleEndian, &startNum) binary.Read(r, binary.LittleEndian, &length) length += 1 for j := uint16(0); j < length; j++ { bm.Add(uint32(key)<<16 | uint32(startNum+j)) } } }
整合:
func main() { data, err := ioutil.ReadFile("../roaring.bin") if err != nil { log.Fatal(err) } r := bytes.NewReader(data) cookie, containerNum, runFlagBitset := readCookieHeader(r) keycards := readDescriptiveHeader(r, containerNum) readOffsetHeader(r, cookie, containerNum) bm := roaring.New() for i := uint32(0); i < uint32(containerNum); i++ { if runFlagBitset != nil && runFlagBitset[i/8]&(1<<(i%8)) != 0 { // run readRunContainer(r, keycards[i].key, bm) } else if keycards[i].card <= 4096 { // array readArrayContainer(r, keycards[i].key, keycards[i].card, bm) } else { // bitmap readBitmapContainer(r, keycards[i].key, keycards[i].card, bm) } } fmt.Println(bm.String()) }
我將寫入讀取那個範例中的 byte.Buffer 儲存到檔案roaring.bin
中。上面的程式就可以解析這個檔案:
12346 1 []
container 0 key 0 card 8
offset 0 16
{1,3,5,7,100,300,500,700}
成功還原了點陣圖
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45