HyperLogLog (HLL) 是用統計方式解決 Count-distinct problem 的資料結構以及演算法,不要求完全正確,而是大概的數量。
演算法其實沒有很難懂,在 2007 年的原始論文「HyperLogLog: the analysis of a near-optimal cardinality estimation algorithm」裡面可以讀到演算法是長這樣:
可以看到一開始要決定好 b
的值 (於是就會有 2b 個 register),以及單個 register M[j]
的大小,所以是一開始就會決定好固定大小,無論有多少元素都會先吃掉這麼多空間。
但在 Redis 的文件「HyperLogLog」裡面則是提到很少元素的時候會低於 12KB:
The magic of this algorithm is that you no longer need to use an amount of memory proportional to the number of items counted, and instead can use a constant amount of memory; 12k bytes in the worst case, or a lot less if your HyperLogLog (We'll just call them HLL from now) has seen very few elements.
網路上搜了一下沒看到怎麼做到的,不過直接翻 Redis 的程式碼 hyperloglog.c 可以看到答案。
在檔案開頭的註解可以看到有 16384 個 register (對應到論文裡面的 b
= 14,因為 214 = 16384),單個 register 的大小則是 6 bit (對應到論文裡面的 M[j]
),相乘後是 12K bytes,剛好符合文件上的說明:
The use of 16384 6-bit registers for a great level of accuracy, using a total of 12k per key.
在「Dense representation」這邊也說明了每個 register 都是 6 bit 的存放方式,到這邊都與 HLL 論文提到的實作一樣。
省空間的方式是在「Sparse representation」這邊做到的,在大多數的 register 都沒有被設定的情況下,用這種方式可以省下大量的空間,而缺點是當元素「有點多」的時候會有比較高的 CPU time:
In the example the sparse representation used just 7 bytes instead of 12k in order to represent the HLL registers. In general for low cardinality there is a big win in terms of space efficiency, traded with CPU time since the sparse representation is slower to access.
依照註解上面的數字,看起來在 10000 個元素以下有機會低於 12KB,然後夠大的時候從 sparse 轉到 dense 上。
本來以為是什麼其他論文可以調整 b
參數 (enlarge),結果是個比較像是 hack 的方式搞定,但的確是蠻有效的...