Redis数据结构—HyperLogLog
一、简单介绍
HyperLogLog是一种用于大规模数据集计数的概率性统计结构,可以统计集合中不同元素的数量,该数据结构有一下特点:
- 内存占用极少,统计上亿甚至几十亿的数量级只需要12KB的内存占用。
- 计算复杂度低,插入、查询操作的复杂复杂度都是O(1)。
- 支持多个数据集的并集:可以将多个集合进行去重合并,并且复杂度是O(1)。
- 结果是估计值,但误差非常小。
二、 应用场景
网站当日访问的去重复IP数量,或者访问某个短视频的用户总数,就是所谓的“唯一元素的计数”。在数学集合论中,基数表示集合中包含唯一元素的个数。
通常,这种技术需要记录截止当前遇到所有的唯一元素,以便在下一个元素到来时,判断是否已经被计数过,仅该元素以前从未出现过才增加计数器。在元素数量大到一定程度之后(比如短视频APP有1亿活跃用户,1亿条视频播放,要实时统计每条视频的UV),无论使用哈希表、搜索树、位图,存储占用消耗都会大到不能接受。
有一类随机算法可以使用估算集合的基数,只使用少量且固定的存储空间,并且插入和统计的算法复杂度都是O(1)。HyperLogLog 算法就非常出色,使用非常少量的内存,同时可以很好地估算基数。HyperLogLog 之所以叫 HyperLogLog,是因为它在 LogLog 算法基础上做的改进,而之前还有Linear Counting算法。
Redis的HyperLogLog仅使用12KB+16B进行计数,标准误差为0.81%,并且可以计数的上限达到了2^64。HyperLogLog 由 Philippe Flajolet 在 原始论文《HyperLogLog: the analysis of a near-optimal cardinality estimation algorithm》 中提出。Redis 中对 HLL 的三个 PFADD/PFCOUNT/PFMERGE,都是以 PF 开头,就是纪念 2011 年已经去世的 Philippe Flajolet 。
2013 年 Google 的 一篇论文《HyperLogLog in Practice: Algorithmic Engineering of a State of The Art Cardinality Estimation Algorithm》 深入介绍了其实际实现和变体。
三、实现原理
先来考虑一个这样的场景:要求你花一段时间来抛硬币,并记录连续抛硬币中连续正面最多的次数,我会用它来猜测你抛硬币的总次数。直观地讲,如果你花了一段时间抛硬币若干次,而你看到的最长连续正面次数是 3,我可以猜到你抛硬币的总次数一定不多。而如果你告诉我你看到了连续 100 个正面朝上,我猜你已经抛硬币很长很长时间了。
当然,你可能很幸运,会在从一开始就连续抛出了 10 次正面,然后停止抛硬币(这是个小概率事件,但是确实可能发生)。那根据这“连续抛出10次正面”的描述,我将会提供一个错的离谱的抛硬币总数的估计值。
所以,可以改进一下这个实验,我会给你10个硬币和和10张纸,你需要轮换着来抛这10枚硬币,每张纸记录每个硬币连续正面最多的次数。这样可以观察到更多的数据,我再评估就会更准确。
HyperLogLog 的原理类似:它对要加入的每个新元素进行 Hash 处理。Hash 的一部分用于索引寄存器(对应前面前面的示例中的某对【硬币+纸对】,作用是将原始集合分成 m 个子集);另一部分用于计算哈希中最长的前导零序列(对应前面硬币连续正面的最大次数)。
HyperLogLog 根据寄存器数组中的值(这些寄存器被设置为迄今为止针对给定子集观察到的最大连续零),计算出估计的基数,并应用修正公式来纠正估计误差, 能够提供非常好的近似基数。
Redis 中,HyperLogLog 使用 64bit 的 Hash 函数,14bit 用于寄存器索引,剩下的 50bit 用于计算前导 0 的个数。具体地,Redis 中 HLL 有 16384(2^14)个寄存器,其中存的值的范围是 0 ~ 50(实际是0~51,在Redis实现 HLL的文章中有介绍)。我们使用 6bit 就可以存储下 0~50 的所有值,所以需要的存储空间是 16384 * 6bit / (8bit/Byte) = 12288 Byte 也就是开头说的 12KB。

实际上 Redis 中对 HyperLogLog 的存储也是个 字符串,只不过这个字符串有个固定格式的头部(16字节)。
struct hllhdr {
char magic[4]; /* "HYLL" */
uint8_t encoding; /* HLL_DENSE or HLL_SPARSE. */
uint8_t notused[3]; /* Reserved for future use, must be zero. */
uint8_t card[8]; /* Cached cardinality, little endian. */
uint8_t registers[]; /* Data bytes. */
};
HyperLogLog 的标准误差为 1.04/sqrt(m),其中 m 是使用的寄存器数量,因此 Redis HLL 标准误差为 0.81%。
关于 HyperLogLog 有三个指令:
PFADD var element1 element2 ...:向一个 key 中加入一个或多个元素PFCOUNT var:获取一个 key 中的基数评估PFCOUNT var1 var2 ...:获取多个 key 合并后的基数评估PFMERGE dst src1 src2 ...:将多个 key 合并成1个存储到 dst 中
3.1 头
通过上边的结构,我们可以看到整个 HLL 的结构有 4 个字段组成的“头”,以及寄存器数组地址。头中有三个有意义的元素:
+------+---+-----+----------+
| HYLL | E | N/U | Cardin. |
+------+---+-----+----------+
- char magic[4]:魔术字符串,固定为 HYLL
- encoding:1字节,标识是何种编码方式
- card: 8 字节,以小端序存储的 64 位整数,用于存储最近计算的基数(Cardinality),其中有 1 bit用来标识基数估算值是否有效。
来实际观察一个 HLL 的 Header:
127.0.0.1:6379> PFADD test_key user1
(integer) 1
127.0.0.1:6379> PFCOUNT test_key
(integer) 1
127.0.0.1:6379> GETRANGE test_key 0 15
"HYLL\x01\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00"
根据前面的介绍可以看到 magic = “HYLL”, encoding = 1 表示是稀疏模式,而 card = 0x01(忽略了高位的0x00),表示基数估算值为 1。
3.2 寄存器数组存储
HLL 使用了 14 位作为寄存器的索引,50位用于计算连续0的个数。因此寄存器有 2^14 => 16384个,而每个寄存器大小 6bit 可以记录 063(实际只会是 051)。
寄存器的两种存储方式。其实稠密和稀疏描述的是寄存器的占用情况,如果16384 个寄存器中极少数有值而大部分是0就会使用稀疏模式,而大多数有值而极少数是0就会使用稠密模式。
3.2.1 稠密模式
稠密模式是为每个寄存器都分配空间,占用空间的总大小是 6 * 16384 / 8 = 12KB,在内存中分布如下:
* The dense representation used by Redis is the following:
*
* +--------+--------+--------+------// //--+
* |11000000|22221111|33333322|55444444 .... |
* +--------+--------+--------+------// //--+
每个字节先使用最低有效位(即从右侧开始使用)。如果一个寄存器会落在两个字节中,则前边一个字节放的是寄存器的低有效位,比如上边第一字节的 11 是 register[1] 的最低两位。前三个字节中寄存器的实际取值的获得方法是:
r0 = r[0] & 63;
r1 = (r[0] >> 6 | r[1] << 2) & 63;
r2 = (r[1] >> 4 | r[2] << 4) & 63;
r3 = (r[2] >> 2) & 63;
3.2.2 稀疏模式
在只有少量寄存器有值的场景中,稀疏模式可以极大地压缩空间,这对内存型数据库的 Redis 来说,非常重要。
来直观地感受一下,以前面只加入一个user1 的 test_key 来观察,总共长度是21,除去16字节头部,仅仅使用了5 字节来代替了 12KB 的存储空间。
127.0.0.1:6379> STRLEN test_key
(integer) 21
127.0.0.1:6379> GETRANGE test_key 16 20
"y\x00\x80F\xfd"
-- 0b01111001 0b00000000 0b10000000 0b01000110 0b11111101
稀疏模式的实现方式比较简单,其实就是将所有寄存器的值进行编码。有三种编码操作符
- ZERO:00xxxxxx,00是前缀,6bit表示连续0的长度。
- XZERO:01xxxxxx yyyyyyyy,01是前缀,14bit表示连续0的长度,最多可以表示16384个。
- VAL:1vvvvvxx,1是前缀,5bit表示要设成的值,2bit表示非0值的连续长度。VAL 能记录的最大值是 5 bit,也就是最大能记录 32,如果连续 0 的超过这个长度,就会转换成稠密模式。
XZERO 中的 X 是 eXtensible 的意思,表示对 ZERO 的扩展,可以处理更长。
根据上边的例子中的 5 个字节,我们可以解析成 3 条操作,即只有下标为 14593 的寄存器是 1:
XZERO: 14593 # 0b01111001 0b00000000
VAL: 1,1 # 0b10000000
XZERO: 1790 # 0b01000110 0b11111101
3.2.3 模式切换
除了上边提到的大数值无法用稀疏模式表示,还要考虑什么情况从稀疏模式切换成稠密模式。
源代码中有给出稀疏模式表示的计数值和存储空间的一个关系,是个大概关系:
| 基数估计值 | 稀疏模式占用空间 |
|---|---|
| 100 | 267 |
| 500 | 1033 |
| 1000 | 1882 |
| 2000 | 3480 |
| 3000 | 4879 |
| 5000 | 7138 |
| 10000 | 10591 |
上边可以看到,在基数是10000左右的时候,稀疏模式占用的空间已经快接近稠密模式的 12KB 了。那是不是在这时候做切换呢?*其实要更早,当稀疏模式所占空间接近稠密模式时,空间上的优势变得很小,但*编码/解码将会带来计算量的增加,相对于稠密模式明显地增加了 CPU 的负担。因此要取得比较明显的优势,需要将这个转换门槛设置的更低。
redis的配置项 hll_sparse_max_bytes 默认是 3000,也就是当稀疏表示的长度超过 3000 时,会转换成稠密模式。
3.3 算法辅助函数
这里所谓“辅助”是相对于后边的“封装”函数一节而言的,但是 HLL 的核心思想也有体现在这些函数中。比如 Hash、000..1 的长度、加入一个新元素等等。Redis 实现 HLL 是分为稠密和稀疏两种模式,两种只是存储方式的区别。因此,为了简洁清晰,这部分也只介绍稠密模式的相关函数。
3.3.1 Hash函数
HyperLogLog 需要一个64位 Hash 函数,Redis 的实现中采用了 MurmurHash2 的 64 位版本。MurmurHash2 是一种非加密的哈希算法,具有良好的分布性和较低的冲突率,同时在处理速度和哈希质量之间做了平衡。实现中进行了端序中立的修改——可以在大端和小端架构中提供相同的结果。
uint64_t MurmurHash64A (const void * key, int len, unsigned int seed)
const void * key, int len 用来表示需要做 Hash 的内容,是典型的 C 语言中表达方式。指针指向数据的起始位置,而长度表示数据的大小或长度。
unsigned int seed 用于初始化哈希算法的种子值,该函数实际被调用的时候,固定传入的是 0xadc83b19ULL。
3.3.2 HLLPatLen 函数
作用:将一个 字符串进行 Hash,获得 64bit Hash 值之后,计算:
-
对应寄存器索引(最低的 14bit。
-
pattern 000..1 的长度(高 50bit),计数是从1开始的,而不是从 0 开始,取值范围可能是 1
HLL_Q。这样做的好处是,寄存器中的值如果是 0 表示还没有进来过元素。这也决定了,后边的 RegHisto 直方统计数组得有 HLL_Q + 2 个元素(0,1HLL_Q)。在计算 pattern 的时候,其实是从最低有效位往最高有效位去判断的,也就是计算最右侧 10..00 连续0的长度。为了简化处理,源码中直接将第 51 位设置成了1(hash |= ((uint64_t)1<<HLL_Q);)
int hllPatLen(unsigned char *ele, size_t elesize, long *regp) {
uint64_t hash, bit, index;
int count;
/* Count the number of zeroes starting from bit HLL_REGISTERS
* (that is a power of two corresponding to the first bit we don't use
* as index). The max run can be 64-P+1 = Q+1 bits.
*
* Note that the final "1" ending the sequence of zeroes must be
* included in the count, so if we find "001" the count is 3, and
* the smallest count possible is no zeroes at all, just a 1 bit
* at the first position, that is a count of 1.
*
* This may sound like inefficient, but actually in the average case
* there are high probabilities to find a 1 after a few iterations. */
hash = MurmurHash64A(ele,elesize,0xadc83b19ULL);
index = hash & HLL_P_MASK; /* Register index. */
hash >>= HLL_P; /* Remove bits used to address the register. */
hash |= ((uint64_t)1<<HLL_Q); /* Make sure the loop terminates
and count will be <= Q+1. */
bit = 1;
count = 1; /* Initialized to 1 since we count the "00000...1" pattern. */
while((hash & bit) == 0) {
count++;
bit <<= 1;
}
*regp = (int) index;
return count;
}
3.3.3 hllDenseSet / hllDenseAdd
作用:在给定的稠密 HLL 寄存器数组中,将指定索引的寄存器的值设置为 count 值,前提是当前值小于给定值。寄存器中存储的,是落到这个寄存器的所有值的最大 hllPatLen 的值。所以假设寄存器原来的值是 4,我们要设置进去一个 2,是不起作用的;只有设置进去一个比 4 大的才会起作用。
而只有当寄存器的值发生变化,计算的近似基数才会发生变化。所以这个函数通过返回 1or0,来表示寄存器是否发生变化,来进一步决定后边是否更新 “Cardin.” 中存储的近似基数。
hllDenseAdd 是对 hllPatLen + hllDenseSet 做了一个简单封装,可以直接通过传入内容,来更新寄存器。
int hllDenseSet(uint8_t *registers, long index, uint8_t count) {
uint8_t oldcount;
HLL_DENSE_GET_REGISTER(oldcount,registers,index);
if (count > oldcount) {
HLL_DENSE_SET_REGISTER(registers,index,count);
return 1;
} else {
return 0;
}
}
3.3.3 hllDenseRegHisto
作用是计算在密集表示中的寄存器直方图。两个入参分别是:
-
registers是包含所有寄存器的数组,总共有 16384 个 6 bits的寄存器 -
reghisto是个数组,用来存储各寄存器的数值的频率;因为每个寄存器 6bit,取值0~63,所以 reghisto 的长度只有 64。但实际上寄存器中的值,只会存0HLL_Q(051),后边的一些bit一定会是0void hllDenseRegHisto(uint8_t *registers, int* reghisto)
3.4 算法实现
依然只关注 稠密模式的处理逻辑。
这个函数实际估算基数值。其实现是使用了 《New cardinality estimation algorithms for HyperLogLog sketches》 一文中的算法 6:

算法中涉及到三个系数:
- hllSigma(),函数
- hllTau(),函数
HLL_ALPHA_INF,宏定义
3.5 Redis命令相关函数
以下两个函数不会被 Redis 命令直接调用,但是会在 HLL 相关指令调用时,被间接用到:
- createHLLObject:创建 HLL 对象,因为是个新的空对象,所以是直接创建一个稀疏模式的对象。
- isHLLObjectOrReply: 判断对象是否是 HLL 对象
3.5.1 pfaddCommand
/* PFADD var ele ele ele … ele => :0 or :1 */
- 检查对象,如不存在则创建,如果存在且非 HLL 对象报错退出
- 针对key,对每个要加入的元素调用
hllAdd,记录更新的次数 - 如果存在更新,将基数 cache 位设置为无效
3.5.2 pfcountCommand
PFCOUNT 支持传入多个key,其作用是计算这多个 key 合并后的基数。因此,单个 key 和多个 key 的逻辑处理不一样。
单个 key 的处理,同样先进行常规的检查。然后计算计数:
- 如果 cache 有效,直接返回 cached 基数
- 如果 cache 失效(由于上边 pfadd 导致的),需要重新计算基数并 cache,返回
3.5.3 pfmergeCommand
/* PFMERGE dest src1 src2 src3 … srcN => OK */
首先创建一个数组,用来存储合并之后的16324 个寄存器的计数值。
然后处理每一个 HLL,调用 hllMerge 计算每个索引寄存器的最大值。
最后将合并之后的寄存器数组转换成 HLL 对象,存储到 dest。
3.6 Cache 的设计
因为基数估算值是根据后边的所有寄存器计算出来的,只寄存器中的值没有发生变化,这个值就不会变,可以直接使用上次计算的结果。
基于此,Redis 的实现中,HLL Header 中的 uint8_t card[8] 实际是个 Cache 值,而其中有一位就是用来标记 Cache 是否失效的(card[7] 的最高位):
#define HLL_VALID_CACHE(hdr) (((hdr)->card[7] & (1<<7)) == 0)
而当调用 PFADD 并且导致寄存器更新的时候,这个有效位会被置成无效,直到下次 PFCOUNT 时,判断需要重新计算,并将有效位置成有效。
该逻辑我们也可以通过 redis-cli 的指令清晰的观察到:
- 调用 PFADD 向空的 HLL 中加入一个元素之后,可以看到 card[7] 的值是 0x80,也就是 0b1000000,此时的估算值cache 是0,但是失效位是1,表示已经失效
- 调用一次 PFCOUNT 之后,card 变成了 1,而最高位变成了0,表示这里边的1是有效的基数估计值的Cache
127.0.0.1:6379> PFADD test_key user1
(integer) 1
127.0.0.1:6379> GETRANGE test_key 8 15
"\x00\x00\x00\x00\x00\x00\x00\x80"
127.0.0.1:6379> PFCOUNT test_key
(integer) 1
127.0.0.1:6379> GETRANGE test_key 8 15
"\x01\x00\x00\x00\x00\x00\x00\x00"
通过重复使用之前计算的基数值,可以节省计算时间和资源,并在实际数据结构没有修改的情况下提供近似的基数估计。这对于需要频繁进行基数估计、频繁进行新值插入的场景都非常有用:
PFADD远多于PFCOUNT,在每次PFADD之后,只是简单的将 chach 标志置为失效,不需要计算PFCOUNT远多于PFADD,在绝大部分PFCOUNT都是直接读取的缓存值,而无需计算
在以上三个函数中,我们都能看到,如果 HLL 对象发生变化,都会调用一段类似于这样的代码:
signalModifiedKey(c,c->db,c->argv[1]);
notifyKeyspaceEvent(NOTIFY_STRING,"pfadd",c->argv[1],c->db->id);
server.dirty += updated;
其中,c->db 是数据库的引用,c->db->id 是对应数据库的 ID。
这三个函数在Redis中的作用如下:
signalModifiedKey(c,c->db,c->argv[1]):用于通知 Redis 系统,一个键(c->argv[1])已经被修改了。这是Redis内部用于维护数据一致性的一种机制。notifyKeyspaceEvent(NOTIFY_STRING,"pfadd",c->argv[1],c->db->id):用于触发一个键空间事件,通知所有订阅了这个事件的客户端。在这个例子中,事件类型是NOTIFY_STRING,事件名称是pfadd,关联的键是c->argv[1]。server.dirty++;:这个语句用于增加服务器的 dirty 寄存器。在Redis中,该寄存器用于跟踪自上次保存以来数据库被修改的次数。每当一个写命令成功执行,这个寄存器就会增加。这个寄存器用于决定何时触发自动保存(AOF 或 RDB)。
另外,还有两个在函数进入时可能会调用的指令:
robj *o = lookupKeyWrite(c->db,c->argv[1]);
robj *o = lookupKeyRead(c->db,c->argv[j]);
这两个函数用于在数据库中查找一个键,并准备对它进行写/读操作。如果键存在,它返回一个指向该键的对象的指针。如果键不存在,它返回NULL。
两个函数的主要区别在于:lookupKeyWrite 会对键进行写入前的准备(例如,如果键被设置为只读,该函数会返回错误),而 lookupKeyRead 则不会。
附录
-
带部分注释的源码:
/*hyperloglog.c -Redis HyperLogLog 概率基数近似。 *该文件实现了算法和导出的Redis命令。 * *版权所有 (c) 2014 年至今,Redis Ltd. *版权所有。 * *根据您选择的 Redis 源可用许可证 2.0 获得许可 *(RSALv2) 或服务器端公共许可证 v1 (SSPLv1)。 */ #include "server.h" #include <stdint.h> #include <math.h> /*Redis HyperLogLog 实现基于以下思想: * **使用[1]中提出的64位散列函数,以估计 *基数大于 10^9,每个基数仅需要 1 个额外位 * 登记。 **使用 16384 个 6 位寄存器来实现很高的精度,使用 *每个键总共 12k。 **使用Redis字符串数据类型。没有引入新的类型。 **没有尝试像[1]中那样压缩数据结构。还有 *使用的算法是[2]中原始的HyperLogLog算法,其中 *唯一的区别是使用了 64 位哈希函数,因此无需更正 *对于接近 2^32 的值执行,如 [1] 中所示。 * *[1] Heule、Nunkesser、Hall:HyperLogLog 实践:算法 *最先进的基数估计算法的工程。 * *[2] P. Flajolet、Éric Fusy、O. Gandouet 和 F. Meunier。超级日志: *近乎最优基数估计算法的分析。 * *Redis使用两种表示方式: * *1)“密集”表示,其中每个条目都由 *6 位整数。 *2) 使用合适的游程压缩的“稀疏”表示 *用于表示 HyperLogLogs,其中许多寄存器设置为 0 *一种有效的内存方式。 * * *HLL 标头 *=== * *密集表示和稀疏表示都有一个 16 字节的标头,如下所示: * *+------+---+-----+----------+ *|希尔 |电子|不适用 |卡丹。 | *+------+---+-----+----------+ * *前 4 个字节是设置为字节“HYLL”的魔术字符串。 *“E”是一字节编码,当前设置为HLL_DENSE或 *HLL_SPARSE。 N/U 是三个未使用的字节。 * *“卡丹”。字段是以小端格式存储的 64 位整数 *计算出最新的基数,如果数据可以重用 *自上次计算以来结构没有被修改(这很有用 *因为 HLLADD 操作很可能不会 *修改实际的数据结构,从而修改近似的基数)。 * *当缓存的最高有效字节中的最高有效位 *设置了基数,意味着数据结构被修改了 *我们不能重用必须重新计算的缓存值。 * *密集表示 *=== * *Redis使用的密集表示如下: * *+--------+--------+--------+--------////--+ *|11000000|22221111|33333322|55444444 .... | *+--------+--------+--------+--------////--+ * *6 位计数器从 1 开始依次编码 *LSB 到 MSB,并根据需要使用接下来的字节。 * *稀疏表示 *=== * *稀疏表示使用游程长度对寄存器进行编码 *编码由三个操作码组成,两个使用一个字节,一个使用 *两个字节。操作码称为 ZERO、XZERO 和 VAL。 * *零操作码表示为 00xxxxxx。表示的6位整数 *由六位'xxxxxx'加1,表示有N个寄存器设置 *到 0。该操作码可以代表 1 到 64 个连续的寄存器集 *为 0 值。 * *XZERO 操作码由两个字节 01xxxxxx yyyyyyyy 表示。 14位 *由位“xxxxxx”表示的整数作为最高有效位,并且 *'yyyyyyyy' 作为最低有效位,加 1,表示有 N *寄存器设置为0。该操作码可以表示从0到16384的连续 *寄存器的值设置为 0。 * *VAL 操作码表示为 1vvvvvxx。它包含一个5位整数 *代表寄存器的值,2位整数代表 *设置为该值“vvvvv”的连续寄存器的数量。 *要获取值和游程长度,整数 vvvvv 和 xx 必须是 *加一。该操作码可以表示从 1 到 32 的值, *重复 1 至 4 次。 * *稀疏表示不能表示数值更大的寄存器 *大于 32,但是我们不太可能在某个寄存器中找到这样的寄存器 *HLL 具有基数,其中稀疏表示仍然更多 *比密集表示的内存效率更高。当这种情况发生时 *HLL 转换为稠密表示。 * *稀疏表示纯粹是位置性的。例如稀疏 *空 HLL 的表示就是:XZERO:16384。 * *HLL 在位置 1000、1020、1021 处仅具有 3 个非零寄存器 *分别设置为2、3、3,则用以下三个表示 *操作码: * *XZERO:1000(寄存器0-999设置为0) *VAL:2,1(1个寄存器设置为值2,即寄存器1000) *ZERO:19(寄存器1001-1019设置为0) *VAL:3,2(2个寄存器设置为值3,即寄存器1020,1021) *XZERO:15362(寄存器1022-16383设置为0) * *在示例中,稀疏表示仅使用 7 个字节 *12k 以便表示 HLL 寄存器。一般情况下对于低 *基数在空间效率方面有很大的胜利,可交易 *与 CPU 时间有关,因为稀疏表示的访问速度较慢。 * *下表显示平均基数与使用的字节数,100 *每个基数的样本(当集合不可表示时,因为 *对于值太大的寄存器,使用了密集表示大小 *作为示例)。 * *100267 *200 485 *300 678 *400 859 *500 1033 *600 1205 *700 1375 *800 1544 *900 1713 *1000 1882 *2000 3480 *3000 4879 *4000 6089 *5000 7138 *6000 8042 *7000 8823 *8000 9500 *9000 10088 *10000 10591 * *密集表示使用 12288 字节,因此有一个很大的胜利 *基数约为 2000-3000。对于更大的基数,常数倍 *参与更新稀疏表示是不合理的 *节省内存。稀疏表示的确切最大长度 *当这个实现切换到密集表示时是 *通过定义 server.hll_sparse_max_bytes 配置。 */ struct hllhdr { char magic[4]; /* "HYLL" */ uint8_t encoding; /* HLL_DENSE or HLL_SPARSE. */ uint8_t notused[3]; /*保留供将来使用,必须为零。*/ uint8_t card[8]; /*缓存基数,小端。*/ uint8_t registers[]; /* Data bytes. */ }; /*缓存的基数 MSB 用于指示缓存值的有效性。*/ #define HLL_INVALIDATE_CACHE(hdr) (hdr)->card[7] |= (1 << 7) #define HLL_VALID_CACHE(hdr) (((hdr)->card[7] & (1 << 7)) == 0) #define HLL_P 14 /*P越大,误差越小。*/ #define HLL_Q (64 - HLL_P) /*用于的哈希值的位数 \ 确定前导零的数量。*/ #define HLL_REGISTERS (1 << HLL_P) /*当 P=14 时,16384 个寄存器。*/ #define HLL_P_MASK (HLL_REGISTERS - 1) /* Mask to index register. */ #define HLL_BITS 6 /* Enough to count up to 63 leading zeroes. */ #define HLL_REGISTER_MAX ((1 << HLL_BITS) - 1) #define HLL_HDR_SIZE sizeof(struct hllhdr) #define HLL_DENSE_SIZE (HLL_HDR_SIZE + ((HLL_REGISTERS * HLL_BITS + 7) / 8)) #define HLL_DENSE 0 /* Dense encoding. */ #define HLL_SPARSE 1 /* Sparse encoding. */ #define HLL_RAW 255 /* Only used internally, never exposed. */ #define HLL_MAX_ENCODING 1 static char *invalid_hll_err = "-INVALIDOBJ Corrupted HLL object detected"; /*============================= 低级位宏 ================= ======== */ /*用于访问密集表示的宏。 * *我们需要在 8 位字节数组中获取和设置 6 位计数器。 *我们使用宏来确保代码内联,因为速度至关重要 *特别是为了计算近似基数 *HLLCOUNT,我们需要一次访问所有寄存器。 *出于同样的原因,我们也希望避免在此代码路径中出现条件。 * *+--------+--------+--------+--------// *|11000000|22221111|33333322|55444444 *+--------+--------+--------+--------// * *注:以上表示中最高有效位(MSB) 每个字节的 *位于左侧。我们开始使用从 LSB 到 MSB 的位, *等等传递到下一个字节。 * *例如,我们想要访问 pos = 1 处的计数器(“111111” *上图)。 * *包含我们数据的第一个字节 b0 的索引是: * *b0 = 6 *位置 /8 = 0 * *+--------+ *|11000000| <-我们的字节位于 b0 *+--------+ * *字节中第一位(从 LSB = 0 开始计数)的位置 *是(谁)给的: * *FB = 6 *POS % 8 -> 6 * *右移“fb”位的 b0。 * *+--------+ *|11000000| <-b0 的初始值 *|00000011| <-右移 6 位后。 *+--------+ * *位 8-fb 位左移 b1 位(2 位) * *+--------+ *|22221111| <-b1 的初始值 *|22111100| <-左移 2 位后。 *+--------+ * *两位或,最后与 111111(十进制 63)进行与 *清理我们不感兴趣的高阶位: * *+--------+ *|00000011| <-b0 右移 *|22111100| <-b1 左移 *|22111111| <-b0 或 b1 *| 111111| <-(b0 OR b1) AND 63,我们的值。 *+--------+ * *我们可以尝试使用不同的示例,例如 pos = 0。在本例中 *6 位计数器实际上包含在一个字节中。 * *b0 = 6 *位置 /8 = 0 * *+--------+ *|11000000| <-我们的字节位于 b0 *+--------+ * *FB = 6 *POS % 8 = 0 * *所以我们右移 0 位(实际上没有移位)并且 *左移下一个8位字节,即使我们不使用它, *但这具有清除位的效果,因此结果 *OR 后不会受到影响。 * *------------------------------------------------------------------------- * *设置寄存器有点复杂,我们假设'val' *是我们要设置的值,已经在正确的范围内了。 * *我们需要两个步骤,第一步我们需要清除这些位,第二步 *我们需要对新位进行按位或运算。 * *让我们尝试使用 'pos' = 1,因此 'b' 处的第一个字节是 0, * *在本例中“fb”为 6。 * *+--------+ *|11000000| <-我们的字节位于 b0 *+--------+ * *要创建一个 AND 掩码来清除该位置的位,我们只需 *用值 63 初始化掩码,左移“fs”位, *最后将结果取反。 * *+--------+ *|00111111| <-“面具”从 63 开始 *|11000000| <-“ls”位左移后的“掩码”。 *|00111111| <-反转后的“掩码”。 *+--------+ * *现在我们可以将“b”处的字节与掩码进行按位与,然后按位或 *它与“val”左移“ls”位来设置新位。 * *现在让我们关注下一个字节b1: * *+--------+ *|22221111| <-b1 的初始值 *+--------+ * *为了构建 AND 掩码,我们再次从 63 值开始,右移 *将其乘以 8-fb 位,并将其反转。 * *+--------+ *|00111111| <-“掩码”设置为 2&6-1 *|00001111| <-右移 8-fb = 2 位后的“掩码” *|11110000| <-按位非后的“掩码”。 *+--------+ * *现在我们可以用 b+1 屏蔽它以清除旧位,然后按位或 *将“val”左移“rs”位以设置新值。 */ /*注意:如果我们访问最后一个计数器,我们还将访问 b+1 字节 *不在数组中,但 sds 字符串总是有一个隐式 null *term,所以该字节存在,我们可以跳过条件句(或者需要 *更明确地分配 1 个字节)。*/ /*将位置“regnum”处的寄存器值存储到变量“target”中。 *'p' 是无符号字节数组。*/ #define HLL_DENSE_GET_REGISTER(target, p, regnum) \ do \ { \ uint8_t *_p = (uint8_t *)p; \ unsigned long _byte = regnum * HLL_BITS / 8; \ unsigned long _fb = regnum * HLL_BITS & 7; \ unsigned long _fb8 = 8 - _fb; \ unsigned long b0 = _p[_byte]; \ unsigned long b1 = _p[_byte + 1]; \ target = ((b0 >> _fb) | (b1 << _fb8)) & HLL_REGISTER_MAX; \ } while (0) /*将位置“regnum”处的寄存器的值设置为“val”。 *'p' 是无符号字节数组。*/ #define HLL_DENSE_SET_REGISTER(p, regnum, val) \ do \ { \ uint8_t *_p = (uint8_t *)p; \ unsigned long _byte = (regnum) * HLL_BITS / 8; \ unsigned long _fb = (regnum) * HLL_BITS & 7; \ unsigned long _fb8 = 8 - _fb; \ unsigned long _v = (val); \ _p[_byte] &= ~(HLL_REGISTER_MAX << _fb); \ _p[_byte] |= _v << _fb; \ _p[_byte + 1] &= ~(HLL_REGISTER_MAX >> _fb8); \ _p[_byte + 1] |= _v >> _fb8; \ } while (0) /*用于访问稀疏表示的宏。 *宏参数应该是一个 uint8_t 指针。*/ #define HLL_SPARSE_XZERO_BIT 0x40 /* 01xxxxxx */ #define HLL_SPARSE_VAL_BIT 0x80 /* 1vvvvvxx */ #define HLL_SPARSE_IS_ZERO(p) (((*(p)) & 0xc0) == 0) /* 00xxxxxx */ #define HLL_SPARSE_IS_XZERO(p) (((*(p)) & 0xc0) == HLL_SPARSE_XZERO_BIT) #define HLL_SPARSE_IS_VAL(p) ((*(p)) & HLL_SPARSE_VAL_BIT) #define HLL_SPARSE_ZERO_LEN(p) (((*(p)) & 0x3f) + 1) #define HLL_SPARSE_XZERO_LEN(p) (((((*(p)) & 0x3f) << 8) | (*((p) + 1))) + 1) #define HLL_SPARSE_VAL_VALUE(p) ((((*(p)) >> 2) & 0x1f) + 1) #define HLL_SPARSE_VAL_LEN(p) (((*(p)) & 0x3) + 1) #define HLL_SPARSE_VAL_MAX_VALUE 32 #define HLL_SPARSE_VAL_MAX_LEN 4 #define HLL_SPARSE_ZERO_MAX_LEN 64 #define HLL_SPARSE_XZERO_MAX_LEN 16384 #define HLL_SPARSE_VAL_SET(p, val, len) \ do \ { \ *(p) = (((val)-1) << 2 | ((len)-1)) | HLL_SPARSE_VAL_BIT; \ } while (0) #define HLL_SPARSE_ZERO_SET(p, len) \ do \ { \ *(p) = (len)-1; \ } while (0) #define HLL_SPARSE_XZERO_SET(p, len) \ do \ { \ int _l = (len)-1; \ *(p) = (_l >> 8) | HLL_SPARSE_XZERO_BIT; \ *((p) + 1) = (_l & 0xff); \ } while (0) #define HLL_ALPHA_INF 0.721347520444481703680 /*0.5/ln(2) 的常数*/ /*========================= HyperLogLog算法 ===================== ==== */ /*我们的哈希函数是 MurmurHash2,64 位版本。 *为了在 Redis 中提供相同的结果,对其进行了修改 *大字节序和小字节序拱门(字节序中立)。*/ REDIS_NO_SANITIZE("alignment") uint64_t MurmurHash64A(const void *key, int len, unsigned int seed) { const uint64_t m = 0xc6a4a7935bd1e995; const int r = 47; uint64_t h = seed ^ (len * m); const uint8_t *data = (const uint8_t *)key; const uint8_t *end = data + (len - (len & 7)); while (data != end) { uint64_t k; #if (BYTE_ORDER == LITTLE_ENDIAN) #ifdef USE_ALIGNED_ACCESS memcpy(&k, data, sizeof(uint64_t)); #else k = *((uint64_t *)data); #endif #else k = (uint64_t)data[0]; k |= (uint64_t)data[1] << 8; k |= (uint64_t)data[2] << 16; k |= (uint64_t)data[3] << 24; k |= (uint64_t)data[4] << 32; k |= (uint64_t)data[5] << 40; k |= (uint64_t)data[6] << 48; k |= (uint64_t)data[7] << 56; #endif k *= m; k ^= k >> r; k *= m; h ^= k; h *= m; data += 8; } switch (len & 7) { case 7: h ^= (uint64_t)data[6] << 48; /* fall-thru */ case 6: h ^= (uint64_t)data[5] << 40; /* fall-thru */ case 5: h ^= (uint64_t)data[4] << 32; /* fall-thru */ case 4: h ^= (uint64_t)data[3] << 24; /* fall-thru */ case 3: h ^= (uint64_t)data[2] << 16; /* fall-thru */ case 2: h ^= (uint64_t)data[1] << 8; /* fall-thru */ case 1: h ^= (uint64_t)data[0]; h *= m; /* fall-thru */ }; h ^= h >> r; h *= m; h ^= h >> r; return h; } /*给定要添加到 HyperLogLog 的字符串元素,返回长度 *元素哈希的模式 000..1。作为副作用“regp”是 *设置为该元素散列到的寄存器索引。*/ int hllPatLen(unsigned char *ele, size_t elesize, long *regp) { uint64_t hash, bit, index; int count; /*计算从 HLL_REGISTERS 位开始的零的数量 *(这是对应于我们不使用的第一位的二的幂 *作为索引)。最大运行可以是 64-P+1 = Q+1 位。 * *请注意,结束零序列的最后一个“1”必须是 *包含在计数中,因此如果我们找到“001”,则计数为 3,并且 *可能的最小计数根本没有零,只有 1 位 *在第一个位置,即计数为 1。 * *这听起来似乎效率很低,但实际上在平均情况下 *经过几次迭代后,很有可能找到 1。*/ hash = MurmurHash64A(ele, elesize, 0xadc83b19ULL); index = hash & HLL_P_MASK; /* Register index. */ hash >>= HLL_P; /* Remove bits used to address the register. */ hash |= ((uint64_t)1 << HLL_Q); /* Make sure the loop terminates and count will be <= Q+1. */ bit = 1; count = 1; /* Initialized to 1 since we count the "00000...1" pattern. */ while ((hash & bit) == 0) { count++; bit <<= 1; } *regp = (int)index; return count; } /*================== 密集表示实现 ================== */ /*将密集 HLL 寄存器设置为“索引”的低级函数 *如果当前值小于“count”,则指定值。 * *“寄存器”预计有空间容纳 HLL_REGISTERS 加上一个 *右侧附加字节。 sds 字符串满足此要求 *自动,因为它们隐式以 null 终止。 * *该函数总是成功,但是如果作为操作的结果 *近似基数发生变化,返回1。否则 0 *被返回。*/ int hllDenseSet(uint8_t *registers, long index, uint8_t count) { uint8_t oldcount; HLL_DENSE_GET_REGISTER(oldcount, registers, index); if (count > oldcount) { HLL_DENSE_SET_REGISTER(registers, index, count); return 1; } else { return 0; } } /*在密集的 hyperloglog 数据结构中“添加”元素。 *实际上什么也没添加,只是子集的最大0模式计数器 *如果需要,元素所属的元素会递增。 * *这只是 hllDenseSet() 的包装,执行哈希 *元素以检索索引和零游程计数。*/ int hllDenseAdd(uint8_t *registers, unsigned char *ele, size_t elesize) { long index; uint8_t count = hllPatLen(ele, elesize, &index); /* Update the register if this element produced a longer run of zeroes. */ return hllDenseSet(registers, index, count); } /* Compute the register histogram in the dense representation. */ void hllDenseRegHisto(uint8_t *registers, int *reghisto) { int j; /*Redis 默认使用 16384 个寄存器,每个寄存器 6 位。代码有效 *通过修改定义使用其他值,但对于我们的目标值 *我们采用展开循环的更快路径。*/ if (HLL_REGISTERS == 16384 && HLL_BITS == 6) { uint8_t *r = registers; unsigned long r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15; for (j = 0; j < 1024; j++) { /* Handle 16 registers per iteration. */ r0 = r[0] & 63; r1 = (r[0] >> 6 | r[1] << 2) & 63; r2 = (r[1] >> 4 | r[2] << 4) & 63; r3 = (r[2] >> 2) & 63; r4 = r[3] & 63; r5 = (r[3] >> 6 | r[4] << 2) & 63; r6 = (r[4] >> 4 | r[5] << 4) & 63; r7 = (r[5] >> 2) & 63; r8 = r[6] & 63; r9 = (r[6] >> 6 | r[7] << 2) & 63; r10 = (r[7] >> 4 | r[8] << 4) & 63; r11 = (r[8] >> 2) & 63; r12 = r[9] & 63; r13 = (r[9] >> 6 | r[10] << 2) & 63; r14 = (r[10] >> 4 | r[11] << 4) & 63; r15 = (r[11] >> 2) & 63; reghisto[r0]++; reghisto[r1]++; reghisto[r2]++; reghisto[r3]++; reghisto[r4]++; reghisto[r5]++; reghisto[r6]++; reghisto[r7]++; reghisto[r8]++; reghisto[r9]++; reghisto[r10]++; reghisto[r11]++; reghisto[r12]++; reghisto[r13]++; reghisto[r14]++; reghisto[r15]++; r += 12; } } else { for (j = 0; j < HLL_REGISTERS; j++) { unsigned long reg; HLL_DENSE_GET_REGISTER(reg, registers, j); reghisto[reg]++; } } } /*================== 稀疏表示实现 ================= */ /*将稀疏表示转换为稠密表示形式作为输入 *代表。两种表示形式均由 SDS 字符串表示,并且 *作为副作用,输入表示被释放。 * *如果稀疏表示有效,该函数返回 C_OK, *否则,如果表示损坏,则返回 C_ERR。*/ int hllSparseToDense(robj *o) { sds sparse = o->ptr, dense; struct hllhdr *hdr, *oldhdr = (struct hllhdr *)sparse; int idx = 0, runlen, regval; uint8_t *p = (uint8_t *)sparse, *end = p + sdslen(sparse); /* If the representation is already the right one return ASAP. */ hdr = (struct hllhdr *)sparse; if (hdr->encoding == HLL_DENSE) return C_OK; /* Create a string of the right size filled with zero bytes. * Note that the cached cardinality is set to 0 as a side effect * that is exactly the cardinality of an empty HLL. */ dense = sdsnewlen(NULL, HLL_DENSE_SIZE); hdr = (struct hllhdr *)dense; *hdr = *oldhdr; /* This will copy the magic and cached cardinality. */ hdr->encoding = HLL_DENSE; /* Now read the sparse representation and set non-zero registers * accordingly. */ p += HLL_HDR_SIZE; while (p < end) { if (HLL_SPARSE_IS_ZERO(p)) { runlen = HLL_SPARSE_ZERO_LEN(p); idx += runlen; p++; } else if (HLL_SPARSE_IS_XZERO(p)) { runlen = HLL_SPARSE_XZERO_LEN(p); idx += runlen; p += 2; } else { runlen = HLL_SPARSE_VAL_LEN(p); regval = HLL_SPARSE_VAL_VALUE(p); if ((runlen + idx) > HLL_REGISTERS) break; /* Overflow. */ while (runlen--) { HLL_DENSE_SET_REGISTER(hdr->registers, idx, regval); idx++; } p++; } } /* If the sparse representation was valid, we expect to find idx * set to HLL_REGISTERS. */ if (idx != HLL_REGISTERS) { sdsfree(dense); return C_ERR; } /* Free the old representation and set the new one. */ sdsfree(o->ptr); o->ptr = dense; return C_OK; } /*将稀疏 HLL 寄存器设置为“索引”的低级函数 *如果当前值小于“count”,则指定值。 * *对象“o”是保存 HLL 的 String 对象。该功能需要 *对对象的引用,以便能够放大字符串,如果 *需要。 * *成功时,如果基数发生变化,函数返回 1,否则返回 0 *如果该元素的寄存器未更新。 *出错时(如果表示无效)返回-1。 * *作为副作用,该函数可能会提升 HLL 表示形式 *稀疏到密集:当寄存器需要设置一个值时会发生这种情况 *无法用稀疏表示来表示,或者当结果 *大小将大于 server.hll_sparse_max_bytes。*/ int hllSparseSet(robj *o, long index, uint8_t count) { struct hllhdr *hdr; uint8_t oldcount, *sparse, *end, *p, *prev, *next; long first, span; long is_zero = 0, is_xzero = 0, is_val = 0, runlen = 0; /*如果计数太大而无法用稀疏表示表示 *切换到密集表示。*/ if (count > HLL_SPARSE_VAL_MAX_VALUE) goto promote; /*当更新稀疏表示时,有时我们可能需要放大 *最坏情况下最多可容纳 3 个字节的缓冲区(XZERO 拆分为 XZERO-VAL-XZERO), *下面的代码完成放大工作。 *实际上,我们使用贪心策略,放大超过3个字节以避免需要 *未来根据增量增长进行重新分配。但我们不会分配超过 *稀疏表示的“server.hll_sparse_max_bytes”字节。 *如果hyperloglog sds字符串的可用大小不足以增量 *我们需要,我们在“步骤 3”中将 hypreloglog 提升为密集表示。 */ if (sdsalloc(o->ptr) < server.hll_sparse_max_bytes && sdsavail(o->ptr) < 3) { size_t newlen = sdslen(o->ptr) + 3; newlen += min(newlen, 300); /* Greediness: double 'newlen' if it is smaller than 300, or add 300 to it when it exceeds 300 */ if (newlen > server.hll_sparse_max_bytes) newlen = server.hll_sparse_max_bytes; o->ptr = sdsResize(o->ptr, newlen, 1); } /* Step 1: we need to locate the opcode we need to modify to check * if a value update is actually needed. */ sparse = p = ((uint8_t *)o->ptr) + HLL_HDR_SIZE; end = p + sdslen(o->ptr) - HLL_HDR_SIZE; first = 0; prev = NULL; /* Points to previous opcode at the end of the loop. */ next = NULL; /* Points to the next opcode at the end of the loop. */ span = 0; while (p < end) { long oplen; /* Set span to the number of registers covered by this opcode. * * This is the most performance critical loop of the sparse * representation. Sorting the conditionals from the most to the * least frequent opcode in many-bytes sparse HLLs is faster. */ oplen = 1; if (HLL_SPARSE_IS_ZERO(p)) { span = HLL_SPARSE_ZERO_LEN(p); } else if (HLL_SPARSE_IS_VAL(p)) { span = HLL_SPARSE_VAL_LEN(p); } else { /* XZERO. */ span = HLL_SPARSE_XZERO_LEN(p); oplen = 2; } /* Break if this opcode covers the register as 'index'. */ if (index <= first + span - 1) break; prev = p; p += oplen; first += span; } if (span == 0 || p >= end) return -1; /* Invalid format. */ next = HLL_SPARSE_IS_XZERO(p) ? p + 2 : p + 1; if (next >= end) next = NULL; /* Cache current opcode type to avoid using the macro again and * again for something that will not change. * Also cache the run-length of the opcode. */ if (HLL_SPARSE_IS_ZERO(p)) { is_zero = 1; runlen = HLL_SPARSE_ZERO_LEN(p); } else if (HLL_SPARSE_IS_XZERO(p)) { is_xzero = 1; runlen = HLL_SPARSE_XZERO_LEN(p); } else { is_val = 1; runlen = HLL_SPARSE_VAL_LEN(p); } /*第 2 步:循环之后: * *'first' 存储到所覆盖的第一个寄存器的索引 *通过当前操作码,由“p”指向。 * *'next' 和 'prev' 分别存储下一个和上一个操作码, *如果“p”处的操作码分别是最后一个或第一个,则为 NULL。 * *'span' 设置为当前覆盖的寄存器数量 *操作码。 * *有不同情况才能更新数据结构 *就位,无需从头开始生成: * *A) 如果它是一个 VAL 操作码,其值已设置为 >= 我们的“计数” *无论 VAL 游程长度字段如何,都不需要更新。 *在这种情况下,PFADD 返回 0,因为没有执行任何更改。 * *B) 如果它是 VAL 操作码,len = 1(仅代表我们的 *register)并且该值小于'count',我们只是更新它 *因为这是一个微不足道的案例。*/ if (is_val) { oldcount = HLL_SPARSE_VAL_VALUE(p); /* Case A. */ if (oldcount >= count) return 0; /* Case B. */ if (runlen == 1) { HLL_SPARSE_VAL_SET(p, count, 1); goto updated; } } /* C) Another trivial to handle case is a ZERO opcode with a len of 1. * We can just replace it with a VAL opcode with our value and len of 1. */ if (is_zero && runlen == 1) { HLL_SPARSE_VAL_SET(p, count, 1); goto updated; } /*D) 一般情况。 * *其他情况更复杂:我们的寄存器需要更新 *并且当前由 len > 1 的 VAL 操作码表示, *通过 len > 1 的 ZERO 操作码,或通过 XZERO 操作码。 * *在这些情况下,原始操作码必须分成多个 *操作码。最坏的情况是中间的 XZERO 分裂导致 *XZERO -VAL -XZERO,因此得到的序列最大长度为 *5 字节。 * *我们执行分割,将新序列写入“新”缓冲区 *以“newlen”作为长度。随后将新序列插入到位 *旧的,可能将右侧的内容移动几个字节 *如果新序列比旧序列长。*/ uint8_t seq[5], *n = seq; int last = first + span - 1; /* Last register covered by the sequence. */ int len; if (is_zero || is_xzero) { /* Handle splitting of ZERO / XZERO. */ if (index != first) { len = index - first; if (len > HLL_SPARSE_ZERO_MAX_LEN) { HLL_SPARSE_XZERO_SET(n, len); n += 2; } else { HLL_SPARSE_ZERO_SET(n, len); n++; } } HLL_SPARSE_VAL_SET(n, count, 1); n++; if (index != last) { len = last - index; if (len > HLL_SPARSE_ZERO_MAX_LEN) { HLL_SPARSE_XZERO_SET(n, len); n += 2; } else { HLL_SPARSE_ZERO_SET(n, len); n++; } } } else { /* Handle splitting of VAL. */ int curval = HLL_SPARSE_VAL_VALUE(p); if (index != first) { len = index - first; HLL_SPARSE_VAL_SET(n, curval, len); n++; } HLL_SPARSE_VAL_SET(n, count, 1); n++; if (index != last) { len = last - index; HLL_SPARSE_VAL_SET(n, curval, len); n++; } } /* Step 3: substitute the new sequence with the old one. * * Note that we already allocated space on the sds string * calling sdsResize(). */ int seqlen = n - seq; int oldlen = is_xzero ? 2 : 1; int deltalen = seqlen - oldlen; if (deltalen > 0 && sdslen(o->ptr) + deltalen > server.hll_sparse_max_bytes) goto promote; serverAssert(sdslen(o->ptr) + deltalen <= sdsalloc(o->ptr)); if (deltalen && next) memmove(next + deltalen, next, end - next); sdsIncrLen(o->ptr, deltalen); memcpy(p, seq, seqlen); end += deltalen; updated: /* Step 4: Merge adjacent values if possible. * * The representation was updated, however the resulting representation * may not be optimal: adjacent VAL opcodes can sometimes be merged into * a single one. */ p = prev ? prev : sparse; int scanlen = 5; /* Scan up to 5 upcodes starting from prev. */ while (p < end && scanlen--) { if (HLL_SPARSE_IS_XZERO(p)) { p += 2; continue; } else if (HLL_SPARSE_IS_ZERO(p)) { p++; continue; } /* We need two adjacent VAL opcodes to try a merge, having * the same value, and a len that fits the VAL opcode max len. */ if (p + 1 < end && HLL_SPARSE_IS_VAL(p + 1)) { int v1 = HLL_SPARSE_VAL_VALUE(p); int v2 = HLL_SPARSE_VAL_VALUE(p + 1); if (v1 == v2) { int len = HLL_SPARSE_VAL_LEN(p) + HLL_SPARSE_VAL_LEN(p + 1); if (len <= HLL_SPARSE_VAL_MAX_LEN) { HLL_SPARSE_VAL_SET(p + 1, v1, len); memmove(p, p + 1, end - p); sdsIncrLen(o->ptr, -1); end--; /* After a merge we reiterate without incrementing 'p' * in order to try to merge the just merged value with * a value on its right. */ continue; } } } p++; } /* Invalidate the cached cardinality. */ hdr = o->ptr; HLL_INVALIDATE_CACHE(hdr); return 1; promote: /* Promote to dense representation. */ if (hllSparseToDense(o) == C_ERR) return -1; /* Corrupted HLL. */ hdr = o->ptr; /*我们需要调用hllDenseAdd()来执行后面的操作 *转换。然而结果必须是 1,因为如果我们需要 *从稀疏转换为密集需要更新寄存器。 * *请注意,这反过来意味着 PFADD 将确保该命令 *传播到slaves/AOF,所以如果有稀疏->密集 *转换,它也将在所有从站中执行。*/ int dense_retval = hllDenseSet(hdr->registers, index, count); serverAssert(dense_retval == 1); return dense_retval; } /*在稀疏 hyperloglog 数据结构中“添加”元素。 *实际上什么也没添加,只是子集的最大0模式计数器 *如果需要,元素所属的元素会递增。 * *该函数实际上是hllSparseSet()的包装,它只执行 *对元素进行散列以获得索引和零游程长度。*/ int hllSparseAdd(robj *o, unsigned char *ele, size_t elesize) { long index; uint8_t count = hllPatLen(ele, elesize, &index); /* Update the register if this element produced a longer run of zeroes. */ return hllSparseSet(o, index, count); } /* Compute the register histogram in the sparse representation. */ void hllSparseRegHisto(uint8_t *sparse, int sparselen, int *invalid, int *reghisto) { int idx = 0, runlen, regval; uint8_t *end = sparse + sparselen, *p = sparse; while (p < end) { if (HLL_SPARSE_IS_ZERO(p)) { runlen = HLL_SPARSE_ZERO_LEN(p); idx += runlen; reghisto[0] += runlen; p++; } else if (HLL_SPARSE_IS_XZERO(p)) { runlen = HLL_SPARSE_XZERO_LEN(p); idx += runlen; reghisto[0] += runlen; p += 2; } else { runlen = HLL_SPARSE_VAL_LEN(p); regval = HLL_SPARSE_VAL_VALUE(p); idx += runlen; reghisto[regval] += runlen; p++; } } if (idx != HLL_REGISTERS && invalid) *invalid = 1; } /*========================= HyperLogLog 计数 ======================= ======= *这是计算近似计数的算法的核心。 *该函数使用较低级别的hllDenseRegHisto()和hllSparseRegHisto() *作为帮助程序来计算寄存器值部分的直方图 *计算,这是特定于表示的,而其余的都是通用的。*/ /*实现uint8_t数据类型的寄存器直方图计算 *仅在内部用作具有多个键的 PFCOUNT 的加速。*/ void hllRawRegHisto(uint8_t *registers, int *reghisto) { uint64_t *word = (uint64_t *)registers; uint8_t *bytes; int j; for (j = 0; j < HLL_REGISTERS / 8; j++) { if (*word == 0) { reghisto[0] += 8; } else { bytes = (uint8_t *)word; reghisto[bytes[0]]++; reghisto[bytes[1]]++; reghisto[bytes[2]]++; reghisto[bytes[3]]++; reghisto[bytes[4]]++; reghisto[bytes[5]]++; reghisto[bytes[6]]++; reghisto[bytes[7]]++; } word++; } } /*辅助函数 sigma 定义于 *“HyperLogLog 草图的新基数估计算法” *奥特马尔·埃特尔,arXiv:1702.01284*/ double hllSigma(double x) { if (x == 1.) return INFINITY; double zPrime; double y = 1; double z = x; do { x *= x; zPrime = z; z += x * y; y += y; } while (zPrime != z); return z; } /*辅助函数 tau 定义于 *“HyperLogLog 草图的新基数估计算法” *奥特马尔·埃特尔,arXiv:1702.01284*/ double hllTau(double x) { if (x == 0. || x == 1.) return 0.; double zPrime; double y = 1.0; double z = 1 - x; do { x = sqrt(x); zPrime = z; y *= 0.5; z -= pow(1 - x, 2) * y; } while (zPrime != z); return z / 3; } /*根据谐波返回集合的近似基数 *寄存器值的平均值。 “hdr”指向 SDS 的开头 *表示保存 HLL 表示的 String 对象。 * *如果 HLL 对象的稀疏表示无效,则整数 *'invalid' 指向的值设置为非零,否则保持不变。 * *hllCount() 支持 HLL_RAW 的特殊内部编码,即 *是,hdr->registers 将指向 HLL_REGISTERS 元素的 uint8_t 数组。 *这对于在针对多个调用时加速 PFCOUNT 很有用 *键(无需使用 6 位整数编码)。*/ uint64_t hllCount(struct hllhdr *hdr, int *invalid) { double m = HLL_REGISTERS; double E; int j; /* Note that reghisto size could be just HLL_Q+2, because HLL_Q+1 is * the maximum frequency of the "000...1" sequence the hash function is * able to return. However it is slow to check for sanity of the * input: instead we history array at a safe size: overflows will * just write data to wrong, but correctly allocated, places. */ int reghisto[64] = {0}; /* Compute register histogram */ if (hdr->encoding == HLL_DENSE) { hllDenseRegHisto(hdr->registers, reghisto); } else if (hdr->encoding == HLL_SPARSE) { hllSparseRegHisto(hdr->registers, sdslen((sds)hdr) - HLL_HDR_SIZE, invalid, reghisto); } else if (hdr->encoding == HLL_RAW) { hllRawRegHisto(hdr->registers, reghisto); } else { serverPanic("Unknown HyperLogLog encoding in hllCount()"); } /* Estimate cardinality from register histogram. See: * "New cardinality estimation algorithms for HyperLogLog sketches" * Otmar Ertl, arXiv:1702.01284 */ double z = m * hllTau((m - reghisto[HLL_Q + 1]) / (double)m); for (j = HLL_Q; j >= 1; --j) { z += reghisto[j]; z *= 0.5; } z += m * hllSigma(reghisto[0] / (double)m); E = llroundl(HLL_ALPHA_INF * m * m / z); return (uint64_t)E; } /* Call hllDenseAdd() or hllSparseAdd() according to the HLL encoding. */ int hllAdd(robj *o, unsigned char *ele, size_t elesize) { struct hllhdr *hdr = o->ptr; switch (hdr->encoding) { case HLL_DENSE: return hllDenseAdd(hdr->registers, ele, elesize); case HLL_SPARSE: return hllSparseAdd(o, ele, elesize); default: return -1; /* Invalid representation. */ } } /*通过计算 MAX(registers[i],hll[i]) 合并 HyperLogLog 'hll' *具有由“max”指向的 uint8_t HLL_REGISTERS 寄存器数组。 * *hll 对象必须已经通过 isHLLObjectOrReply() 进行验证 *或以其他方式。 * *如果HyperLogLog稀疏,发现无效,C_ERR 返回 *,否则函数始终成功。*/ int hllMerge(uint8_t *max, robj *hll) { struct hllhdr *hdr = hll->ptr; int i; if (hdr->encoding == HLL_DENSE) { uint8_t val; for (i = 0; i < HLL_REGISTERS; i++) { HLL_DENSE_GET_REGISTER(val, hdr->registers, i); if (val > max[i]) max[i] = val; } } else { uint8_t *p = hll->ptr, *end = p + sdslen(hll->ptr); long runlen, regval; p += HLL_HDR_SIZE; i = 0; while (p < end) { if (HLL_SPARSE_IS_ZERO(p)) { runlen = HLL_SPARSE_ZERO_LEN(p); i += runlen; p++; } else if (HLL_SPARSE_IS_XZERO(p)) { runlen = HLL_SPARSE_XZERO_LEN(p); i += runlen; p += 2; } else { runlen = HLL_SPARSE_VAL_LEN(p); regval = HLL_SPARSE_VAL_VALUE(p); if ((runlen + i) > HLL_REGISTERS) break; /* Overflow. */ while (runlen--) { if (regval > max[i]) max[i] = regval; i++; } p++; } } if (i != HLL_REGISTERS) return C_ERR; } return C_OK; } /*============================ HyperLogLog 命令 ====================== ====== */ /*创建一个 HLL 对象。我们始终使用稀疏编码创建 HLL。 *这将根据需要升级为密集表示。*/ robj *createHLLObject(void) { robj *o; struct hllhdr *hdr; sds s; uint8_t *p; int sparselen = HLL_HDR_SIZE + (((HLL_REGISTERS + (HLL_SPARSE_XZERO_MAX_LEN - 1)) / HLL_SPARSE_XZERO_MAX_LEN) * 2); int aux; /* Populate the sparse representation with as many XZERO opcodes as * needed to represent all the registers. */ aux = HLL_REGISTERS; s = sdsnewlen(NULL, sparselen); p = (uint8_t *)s + HLL_HDR_SIZE; while (aux) { int xzero = HLL_SPARSE_XZERO_MAX_LEN; if (xzero > aux) xzero = aux; HLL_SPARSE_XZERO_SET(p, xzero); p += 2; aux -= xzero; } serverAssert((p - (uint8_t *)s) == sparselen); /* Create the actual object. */ o = createObject(OBJ_STRING, s); hdr = o->ptr; memcpy(hdr->magic, "HYLL", 4); hdr->encoding = HLL_SPARSE; return o; } /* Check if the object is a String with a valid HLL representation. * Return C_OK if this is true, otherwise reply to the client * with an error and return C_ERR. */ int isHLLObjectOrReply(client *c, robj *o) { struct hllhdr *hdr; /* Key exists, check type */ if (checkType(c, o, OBJ_STRING)) return C_ERR; /* Error already sent. */ if (!sdsEncodedObject(o)) goto invalid; if (stringObjectLen(o) < sizeof(*hdr)) goto invalid; hdr = o->ptr; /* Magic should be "HYLL". */ if (hdr->magic[0] != 'H' || hdr->magic[1] != 'Y' || hdr->magic[2] != 'L' || hdr->magic[3] != 'L') goto invalid; if (hdr->encoding > HLL_MAX_ENCODING) goto invalid; /* Dense representation string length should match exactly. */ if (hdr->encoding == HLL_DENSE && stringObjectLen(o) != HLL_DENSE_SIZE) goto invalid; /* All tests passed. */ return C_OK; invalid: addReplyError(c, "-WRONGTYPE Key is not a valid " "HyperLogLog string value."); return C_ERR; } /* PFADD var ele ele ele ... ele => :0 or :1 */ void pfaddCommand(client *c) { robj *o = lookupKeyWrite(c->db, c->argv[1]); struct hllhdr *hdr; int updated = 0, j; if (o == NULL) { /*使用精确长度的字符串值创建键 *保存我们的 HLL 数据结构。 sdsnewlen() 当传递 NULL 时 *保证返回初始化为零的字节。*/ o = createHLLObject(); dbAdd(c->db, c->argv[1], o); updated++; } else { if (isHLLObjectOrReply(c, o) != C_OK) return; o = dbUnshareStringValue(c->db, c->argv[1], o); } /* Perform the low level ADD operation for every element. */ for (j = 2; j < c->argc; j++) { int retval = hllAdd(o, (unsigned char *)c->argv[j]->ptr, sdslen(c->argv[j]->ptr)); switch (retval) { case 1: updated++; break; case -1: addReplyError(c, invalid_hll_err); return; } } hdr = o->ptr; if (updated) { HLL_INVALIDATE_CACHE(hdr); signalModifiedKey(c, c->db, c->argv[1]); notifyKeyspaceEvent(NOTIFY_STRING, "pfadd", c->argv[1], c->db->id); server.dirty += updated; } addReply(c, updated ? shared.cone : shared.czero); } /* PFCOUNT var -> approximated cardinality of set. */ void pfcountCommand(client *c) { robj *o; struct hllhdr *hdr; uint64_t card; /*情况 1:多键键,并集基数。 * *当指定多个key时,PFCOUNT实际计算 *指定的 N 个 HLL 的合并基数。*/ if (c->argc > 2) { uint8_t max[HLL_HDR_SIZE + HLL_REGISTERS], *registers; int j; /* Compute an HLL with M[i] = MAX(M[i]_j). */ memset(max, 0, sizeof(max)); hdr = (struct hllhdr *)max; hdr->encoding = HLL_RAW; /* Special internal-only encoding. */ registers = max + HLL_HDR_SIZE; for (j = 1; j < c->argc; j++) { /* Check type and size. */ robj *o = lookupKeyRead(c->db, c->argv[j]); if (o == NULL) continue; /* Assume empty HLL for non existing var.*/ if (isHLLObjectOrReply(c, o) != C_OK) return; /* Merge with this HLL with our 'max' HLL by setting max[i] * to MAX(max[i],hll[i]). */ if (hllMerge(registers, o) == C_ERR) { addReplyError(c, invalid_hll_err); return; } } /* Compute cardinality of the resulting set. */ addReplyLongLong(c, hllCount(hdr, NULL)); return; } /*情况 2:单个 HLL 的基数。 * *用户指定了一个键。要么返回缓存的值 *或计算一个并更新缓存。 * *由于 HLL 是常规 Redis 字符串类型值,因此更新缓存不会 *修改数值。无论如何,我们都会执行lookupKeyRead,因为这被标记为 *只读命令。不同之处在于,使用lookupKeyWrite,a *副本上逻辑过期的键被删除,同时使用lookupKeyRead *不是,但如果键在逻辑上是这样的,则查找无论如何都会返回 NULL *过期了,这才是重要的。*/ o = lookupKeyRead(c->db, c->argv[1]); if (o == NULL) { /* No key? Cardinality is zero since no element was added, otherwise * we would have a key as HLLADD creates it as a side effect. */ addReply(c, shared.czero); } else { if (isHLLObjectOrReply(c, o) != C_OK) return; o = dbUnshareStringValue(c->db, c->argv[1], o); /* Check if the cached cardinality is valid. */ hdr = o->ptr; if (HLL_VALID_CACHE(hdr)) { /* Just return the cached value. */ card = (uint64_t)hdr->card[0]; card |= (uint64_t)hdr->card[1] << 8; card |= (uint64_t)hdr->card[2] << 16; card |= (uint64_t)hdr->card[3] << 24; card |= (uint64_t)hdr->card[4] << 32; card |= (uint64_t)hdr->card[5] << 40; card |= (uint64_t)hdr->card[6] << 48; card |= (uint64_t)hdr->card[7] << 56; } else { int invalid = 0; /* Recompute it and update the cached value. */ card = hllCount(hdr, &invalid); if (invalid) { addReplyError(c, invalid_hll_err); return; } hdr->card[0] = card & 0xff; hdr->card[1] = (card >> 8) & 0xff; hdr->card[2] = (card >> 16) & 0xff; hdr->card[3] = (card >> 24) & 0xff; hdr->card[4] = (card >> 32) & 0xff; hdr->card[5] = (card >> 40) & 0xff; hdr->card[6] = (card >> 48) & 0xff; hdr->card[7] = (card >> 56) & 0xff; /* This is considered a read-only command even if the cached value * may be modified and given that the HLL is a Redis string * we need to propagate the change. */ signalModifiedKey(c, c->db, c->argv[1]); server.dirty++; } addReplyLongLong(c, card); } } /* PFMERGE dest src1 src2 src3 ... srcN => OK */ void pfmergeCommand(client *c) { uint8_t max[HLL_REGISTERS]; struct hllhdr *hdr; int j; int use_dense = 0; /* Use dense representation as target? */ /* Compute an HLL with M[i] = MAX(M[i]_j). * We store the maximum into the max array of registers. We'll write * it to the target variable later. */ memset(max, 0, sizeof(max)); for (j = 1; j < c->argc; j++) { /* Check type and size. */ robj *o = lookupKeyRead(c->db, c->argv[j]); if (o == NULL) continue; /* Assume empty HLL for non existing var. */ if (isHLLObjectOrReply(c, o) != C_OK) return; /* If at least one involved HLL is dense, use the dense representation * as target ASAP to save time and avoid the conversion step. */ hdr = o->ptr; if (hdr->encoding == HLL_DENSE) use_dense = 1; /* Merge with this HLL with our 'max' HLL by setting max[i] * to MAX(max[i],hll[i]). */ if (hllMerge(max, o) == C_ERR) { addReplyError(c, invalid_hll_err); return; } } /* Create / unshare the destination key's value if needed. */ robj *o = lookupKeyWrite(c->db, c->argv[1]); if (o == NULL) { /* Create the key with a string value of the exact length to * hold our HLL data structure. sdsnewlen() when NULL is passed * is guaranteed to return bytes initialized to zero. */ o = createHLLObject(); dbAdd(c->db, c->argv[1], o); } else { /* If key exists we are sure it's of the right type/size * since we checked when merging the different HLLs, so we * don't check again. */ o = dbUnshareStringValue(c->db, c->argv[1], o); } /* Convert the destination object to dense representation if at least * one of the inputs was dense. */ if (use_dense && hllSparseToDense(o) == C_ERR) { addReplyError(c, invalid_hll_err); return; } /* Write the resulting HLL to the destination HLL registers and * invalidate the cached value. */ for (j = 0; j < HLL_REGISTERS; j++) { if (max[j] == 0) continue; hdr = o->ptr; switch (hdr->encoding) { case HLL_DENSE: hllDenseSet(hdr->registers, j, max[j]); break; case HLL_SPARSE: hllSparseSet(o, j, max[j]); break; } } hdr = o->ptr; /* o->ptr may be different now, as a side effect of last hllSparseSet() call. */ HLL_INVALIDATE_CACHE(hdr); signalModifiedKey(c, c->db, c->argv[1]); /* We generate a PFADD event for PFMERGE for semantical simplicity * since in theory this is a mass-add of elements. */ notifyKeyspaceEvent(NOTIFY_STRING, "pfadd", c->argv[1], c->db->id); server.dirty++; addReply(c, shared.ok); } /*============================ 测试/调试 =================== ======= */ /*自我测试 *该命令执行 HLL 寄存器实现的自检。 *从外部不容易测试的东西。*/ #define HLL_TEST_CYCLES 1000 void pfselftestCommand(client *c) { unsigned int j, i; sds bitcounters = sdsnewlen(NULL, HLL_DENSE_SIZE); struct hllhdr *hdr = (struct hllhdr *)bitcounters, *hdr2; robj *o = NULL; uint8_t bytecounters[HLL_REGISTERS]; /*测试 1:访问寄存器。 *该测试旨在测试我们数据的不同计数器 *结构是可访问的并且设置它们的值都会导致 *正确的值被保留并且不影响相邻的值。*/ for (j = 0; j < HLL_TEST_CYCLES; j++) { /* Set the HLL counters and an array of unsigned byes of the * same size to the same set of random values. */ for (i = 0; i < HLL_REGISTERS; i++) { unsigned int r = rand() & HLL_REGISTER_MAX; bytecounters[i] = r; HLL_DENSE_SET_REGISTER(hdr->registers, i, r); } /* Check that we are able to retrieve the same values. */ for (i = 0; i < HLL_REGISTERS; i++) { unsigned int val; HLL_DENSE_GET_REGISTER(val, hdr->registers, i); if (val != bytecounters[i]) { addReplyErrorFormat(c, "TESTFAILED Register %d should be %d but is %d", i, (int)bytecounters[i], (int)val); goto cleanup; } } } /*测试 2:近似误差。 *测试添加独特元素并检查估计值 *始终是合理的界限。 * *我们检查误差比预期小几倍 *标准错误,使测试不太可能失败,因为 *“糟糕”的运行。 * *测试同时使用密集和稀疏 HLL 进行 *时间还验证计算的基数是否相同。*/ memset(hdr->registers, 0, HLL_DENSE_SIZE - HLL_HDR_SIZE); o = createHLLObject(); double relerr = 1.04 / sqrt(HLL_REGISTERS); int64_t checkpoint = 1; uint64_t seed = (uint64_t)rand() | (uint64_t)rand() << 32; uint64_t ele; for (j = 1; j <= 10000000; j++) { ele = j ^ seed; hllDenseAdd(hdr->registers, (unsigned char *)&ele, sizeof(ele)); hllAdd(o, (unsigned char *)&ele, sizeof(ele)); /* Make sure that for small cardinalities we use sparse * encoding. */ if (j == checkpoint && j < server.hll_sparse_max_bytes / 2) { hdr2 = o->ptr; if (hdr2->encoding != HLL_SPARSE) { addReplyError(c, "TESTFAILED sparse encoding not used"); goto cleanup; } } /* Check that dense and sparse representations agree. */ if (j == checkpoint && hllCount(hdr, NULL) != hllCount(o->ptr, NULL)) { addReplyError(c, "TESTFAILED dense/sparse disagree"); goto cleanup; } /* Check error. */ if (j == checkpoint) { int64_t abserr = checkpoint - (int64_t)hllCount(hdr, NULL); uint64_t maxerr = ceil(relerr * 6 * checkpoint); /* Adjust the max error we expect for cardinality 10 * since from time to time it is statistically likely to get * much higher error due to collision, resulting into a false * positive. */ if (j == 10) maxerr = 1; if (abserr < 0) abserr = -abserr; if (abserr > (int64_t)maxerr) { addReplyErrorFormat(c, "TESTFAILED Too big error. card:%llu abserr:%llu", (unsigned long long)checkpoint, (unsigned long long)abserr); goto cleanup; } checkpoint *= 10; } } /* Success! */ addReply(c, shared.ok); cleanup: sdsfree(bitcounters); if (o) decrRefCount(o); } /* Different debugging related operations about the HLL implementation. * * PFDEBUG GETREG <key> * PFDEBUG DECODE <key> * PFDEBUG ENCODING <key> * PFDEBUG TODENSE <key> */ void pfdebugCommand(client *c) { char *cmd = c->argv[1]->ptr; struct hllhdr *hdr; robj *o; int j; o = lookupKeyWrite(c->db, c->argv[2]); if (o == NULL) { addReplyError(c, "The specified key does not exist"); return; } if (isHLLObjectOrReply(c, o) != C_OK) return; o = dbUnshareStringValue(c->db, c->argv[2], o); hdr = o->ptr; /* PFDEBUG GETREG <key> */ if (!strcasecmp(cmd, "getreg")) { if (c->argc != 3) goto arityerr; if (hdr->encoding == HLL_SPARSE) { if (hllSparseToDense(o) == C_ERR) { addReplyError(c, invalid_hll_err); return; } server.dirty++; /* Force propagation on encoding change. */ } hdr = o->ptr; addReplyArrayLen(c, HLL_REGISTERS); for (j = 0; j < HLL_REGISTERS; j++) { uint8_t val; HLL_DENSE_GET_REGISTER(val, hdr->registers, j); addReplyLongLong(c, val); } } /* PFDEBUG DECODE <key> */ else if (!strcasecmp(cmd, "decode")) { if (c->argc != 3) goto arityerr; uint8_t *p = o->ptr, *end = p + sdslen(o->ptr); sds decoded = sdsempty(); if (hdr->encoding != HLL_SPARSE) { sdsfree(decoded); addReplyError(c, "HLL encoding is not sparse"); return; } p += HLL_HDR_SIZE; while (p < end) { int runlen, regval; if (HLL_SPARSE_IS_ZERO(p)) { runlen = HLL_SPARSE_ZERO_LEN(p); p++; decoded = sdscatprintf(decoded, "z:%d ", runlen); } else if (HLL_SPARSE_IS_XZERO(p)) { runlen = HLL_SPARSE_XZERO_LEN(p); p += 2; decoded = sdscatprintf(decoded, "Z:%d ", runlen); } else { runlen = HLL_SPARSE_VAL_LEN(p); regval = HLL_SPARSE_VAL_VALUE(p); p++; decoded = sdscatprintf(decoded, "v:%d,%d ", regval, runlen); } } decoded = sdstrim(decoded, " "); addReplyBulkCBuffer(c, decoded, sdslen(decoded)); sdsfree(decoded); } /* PFDEBUG ENCODING <key> */ else if (!strcasecmp(cmd, "encoding")) { char *encodingstr[2] = {"dense", "sparse"}; if (c->argc != 3) goto arityerr; addReplyStatus(c, encodingstr[hdr->encoding]); } /* PFDEBUG TODENSE <key> */ else if (!strcasecmp(cmd, "todense")) { int conv = 0; if (c->argc != 3) goto arityerr; if (hdr->encoding == HLL_SPARSE) { if (hllSparseToDense(o) == C_ERR) { addReplyError(c, invalid_hll_err); return; } conv = 1; server.dirty++; /* Force propagation on encoding change. */ } addReply(c, conv ? shared.cone : shared.czero); } else { addReplyErrorFormat(c, "Unknown PFDEBUG subcommand '%s'", cmd); } return; arityerr: addReplyErrorFormat(c, "Wrong number of arguments for the '%s' subcommand", cmd); }