Redis之整数集合intset

intset是Redis集合的底层实现之一,当存储整数集合并且数据量较小的情况下Redis会使用intset作为set的底层实现。当数据量较大或者集合元素为字符串时则会使用dict实现set。
intset将整数元素按顺序存储在数组里,并通过二分法降低查找元素的时间复杂度。数据量大时,依赖于“查找”的命令(如SISMEMBER)就会由于O(logn)的时间复杂度而遇到一定的瓶颈,所以数据量大时会用dict来代替intset。但是intset的优势就在于比dict更省内存,而且数据量小的时候O(logn)未必会慢于O(1)的hash function。这也是intset存在的原因。

intset结构体声明

typedef struct intset {uint32_t encoding; //intset的类型编码uint32_t length; //成员元素的个数int8_t contents[];//用来存储成员的柔性数组
}

需要注意contents数组成员被声明为int8_t类型并不表示contents里存的是int8_t类型的成员,这个类型声明对于contents来说可以认为是毫无意义的,因为intset成员是什么类型完全取决于encoding变量的值。encoding提供下面三种值:

#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))

如果intset的encoding为INTSET_ENC_INT16,则contents的每个成员的“逻辑类型”都为int16_t。
虽然每个成员的“实际类型”是int8_t,无法直接通过contents[x]取出索引为x的成员元素,但是intset.c里提供了些函数,可以按照不同的encoding方式设置/取出contents的成员。(用指针设置,memcpy取出)
由于这种方法在内存上暴力地赋值与取值,所以希望元素在不同机器上存储的字节序一致,但是不同处理器 在内存中存放数据的方式不一定相同,主要分为大端字节序和小端字节序。 大小端字节序自己网上搜索资料进行学习。

如果老老实实通过contents[x]的方式赋值取值,我们就不需要考虑这个字节序的问题,但是intset根据encoding的值指定元素的地址偏移,暴力地对内存进行操作。若数据被截断了,则大端机器和小端机器会表现出不统一的状况。为了避免这种情况发生,intset不管在什么机器上都按照同一种字节序(小端)在内存中存intset的成员变量。

redis源码中使用了比较暴力的方式进行大小端的转换:以64位的转换为例.

/* variants of the function doing the actual convertion only if the target* host is big endian */
#if (BYTE_ORDER == LITTLE_ENDIAN)
#define memrev16ifbe(p)
#define memrev32ifbe(p)
#define memrev64ifbe(p)
#define intrev16ifbe(v) (v)
#define intrev32ifbe(v) (v)
#define intrev64ifbe(v) (v)
#else
#define memrev16ifbe(p) memrev16(p)
#define memrev32ifbe(p) memrev32(p)
#define memrev64ifbe(p) memrev64(p)
#define intrev16ifbe(v) intrev16(v)
#define intrev32ifbe(v) intrev32(v)
#define intrev64ifbe(v) intrev64(v)
#endif//翻转
void memrev64(void *p) {unsigned char *x = p, t;t = x[0];x[0] = x[7];x[7] = t;t = x[1];x[1] = x[6];x[6] = t;t = x[2];x[2] = x[5];x[5] = t;t = x[3];x[3] = x[4];x[4] = t;
}
uint64_t intrev64(uint64_t v) {memrev64(&v);return v;
}

intset基本操作

底层赋值/取值操作

通过_intsetSet和_intsetGet这两个工具函数,可以根据intset的encoding 读/写contents里索引为pos的值。这是后续intset操作的基础。

/* Set the value at pos, using the configured encoding. */
//按照intset的encoding设置指定位置pos的值
static void _intsetSet(intset *is, int pos, int64_t value) {uint32_t encoding = intrev32ifbe(is->encoding);if (encoding == INTSET_ENC_INT64) {((int64_t*)is->contents)[pos] = value;//大端机器在设置contents时将数据按字节翻转,按照小端序存储memrev64ifbe(((int64_t*)is->contents)+pos);} else if (encoding == INTSET_ENC_INT32) {((int32_t*)is->contents)[pos] = value;memrev32ifbe(((int32_t*)is->contents)+pos);} else {((int16_t*)is->contents)[pos] = value;memrev16ifbe(((int16_t*)is->contents)+pos);}
}
/* Return the value at pos, using the configured encoding. */
//按照intset的encoding取出指定位置pos的值
//不对pos进行越界判断,可能会导致undefined behavior
static int64_t _intsetGet(intset *is, int pos) {return _intsetGetEncoded(is,pos,intrev32ifbe(is->encoding));
}
/* Return the value at pos, given an encoding. */
//以enc为编码取出整数集合is在pos索引上的值
//不对pos进行越界判断,可能会导致undefined behavior
static int64_t _intsetGetEncoded(intset *is, int pos, uint8_t enc) {int64_t v64;int32_t v32;int16_t v16;if (enc == INTSET_ENC_INT64) {//将contents在pos位置的值赋给v64//不能直接写contents[pos]的原因是contents时int8_t类型的,contents[pos]表示是以sizeof(int8_t)为单位移动的指针,而实际的编码是INTSET_ENC_INT64,先将contents指针的类型变为int64_t*memcpy(&v64,((int64_t*)is->contents)+pos,sizeof(v64));//大端机器在取出contents时将原本按照小端序存储的数据按字节翻转,读出正确的值memrev64ifbe(&v64);return v64;} else if (enc == INTSET_ENC_INT32) {memcpy(&v32,((int32_t*)is->contents)+pos,sizeof(v32));memrev32ifbe(&v32);return v32;} else {memcpy(&v16,((int16_t*)is->contents)+pos,sizeof(v16));memrev16ifbe(&v16);return v16;}
}

创建一个空intset

空intset的默认encoding是INTSET_ENC_INT16,contents每个成员的逻辑类型是int16_t(虽然还没有成员)

/* Create an empty intset. */
intset *intsetNew(void) {intset *is = zmalloc(sizeof(intset));is->encoding = intrev32ifbe(INTSET_ENC_INT16);is->length = 0;return is;
}

查询一个成员

前面说了intset是将元素按大小顺序存储在contents数组里,所以在插入新元素之前,必须通过二分法找到合理的插入位置,这由intsetSearch(intset is, int64_t value, uint32_t pos)函数实现。
它的作用是在整数集合里用二分法找到value的位置,并把位置写给pos参数,函数返回1;若没找到,则写给pos的是能被插入的value的位置(intset按顺序存储),函数返回0。

static uint8_t intsetSearch(intset *is, int64_t value, uint32_t *pos) {int min = 0, max = intrev32ifbe(is->length)-1, mid = -1;int64_t cur = -1;/* The value can never be found when the set is empty */if (intrev32ifbe(is->length) == 0) {if (pos) *pos = 0;return 0;} else {/* Check for the case where we know we cannot find the value,* but do know the insert position. *///判断是否大于小于边界值if (value > _intsetGet(is,intrev32ifbe(is->length)-1)) {if (pos) *pos = intrev32ifbe(is->length);//value可以被插入的位置return 0;} else if (value < _intsetGet(is,0)) {if (pos) *pos = 0;return 0;}}//二分while(max >= min) {mid = ((unsigned int)min + (unsigned int)max) >> 1;cur = _intsetGet(is,mid);if (value > cur) {min = mid+1;} else if (value < cur) {max = mid-1;} else {break;}}if (value == cur) {if (pos) *pos = mid;//找到了return 1;} else {if (pos) *pos = min;return 0;}
}

使用二分法的查找方法还是比较快速的找到对应的值的,上面写的方式在数据量比较大时,可能会存在越界的可能,可以改为下面的方式:

while (max >= min) {mid = min + (max - min)/2;........
}

原因是:max + min可能就越界了。

插入一个成员

插入一个值为value的成员时,会做以下判断逻辑:

  • 计算value的encoding
  • 若value的encoding大于要插入的intset的encoding,则调用intsetUpgradeAndAdd直接升级intset的encoding并插入到首部或者尾部。
  • 若value的encoding小于要插入的intset的encoding,则不需要升级intset的encoding,调用intsetSearch找到合适的插入位置,再将该位置到contents尾部的数据全部右移一格,最后将value插入到pos。
/* Insert an integer in the intset */
//success传null进来则说明外层调用者不需要知道是否插入成功(value是否已存在),否则success用于此目的
intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {uint8_t valenc = _intsetValueEncoding(value);//根据value的大小计算value的encodinguint32_t pos;if (success) *success = 1;/* Upgrade encoding if necessary. If we need to upgrade, we know that* this value should be either appended (if > 0) or prepended (if < 0),* because it lies outside the range of existing values. */if (valenc > intrev32ifbe(is->encoding)) {//这种插入需要改变encoding(不需要search,因为encoding改变说明value一定插入在contents首部或者尾部)/* This always succeeds, so we don't need to curry *success. */return intsetUpgradeAndAdd(is,value);} else {/* Abort if the value is already present in the set.* This call will populate "pos" with the right position to insert* the value when it cannot be found. */if (intsetSearch(is,value,&pos)) {if (success) *success = 0;//intset里已存在该值,返回失败return is;}is = intsetResize(is,intrev32ifbe(is->length)+1);if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);//右移一格}_intsetSet(is,pos,value);//插入值is->length = intrev32ifbe(intrev32ifbe(is->length)+1);return is;
}/* Return the required encoding for the provided value. */
//根据v值的大小决定需要的编码类型
static uint8_t _intsetValueEncoding(int64_t v) {if (v < INT32_MIN || v > INT32_MAX)return INTSET_ENC_INT64;else if (v < INT16_MIN || v > INT16_MAX)return INTSET_ENC_INT32;elsereturn INTSET_ENC_INT16;
}/* Upgrades the intset to a larger encoding and inserts the given integer. */
//这个函数执行的前提是value参数的大小超过了当前编码
//为is->content重新分配内存并修改编码添加value进这个intset
static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {uint8_t curenc = intrev32ifbe(is->encoding);//当前编码类型uint8_t newenc = _intsetValueEncoding(value);//新的编码类型int length = intrev32ifbe(is->length);int prepend = value < 0 ? 1 : 0;//因为value一定超过了编码的限制,所以看value是大于0还是小于0以此决定value放置在content[0]还是content[length]/* First set new encoding and resize */is->encoding = intrev32ifbe(newenc);is = intsetResize(is,intrev32ifbe(is->length)+1);/* Upgrade back-to-front so we don't overwrite values.* Note that the "prepend" variable is used to make sure we have an empty* space at either the beginning or the end of the intset. */while(length--)//以curenc为编码倒序取出所有值并赋值给新的位置_intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));/* Set the value at the beginning or the end. */if (prepend)_intsetSet(is,0,value);else_intsetSet(is,intrev32ifbe(is->length),value);is->length = intrev32ifbe(intrev32ifbe(is->length)+1);return is;
}/* Resize the intset */
//解除is的内存分配并重新分配长度为len的intset的内存
static intset *intsetResize(intset *is, uint32_t len) {uint32_t size = len*intrev32ifbe(is->encoding);is = zrealloc(is,sizeof(intset)+size);return is;
}//把from索引到intset尾部的整块数据复制to索引(复制之后from值不变,但是可以被覆盖)
static void intsetMoveTail(intset *is, uint32_t from, uint32_t to) {void *src, *dst;uint32_t bytes = intrev32ifbe(is->length)-from;uint32_t encoding = intrev32ifbe(is->encoding);if (encoding == INTSET_ENC_INT64) {src = (int64_t*)is->contents+from;dst = (int64_t*)is->contents+to;bytes *= sizeof(int64_t);} else if (encoding == INTSET_ENC_INT32) {src = (int32_t*)is->contents+from;dst = (int32_t*)is->contents+to;bytes *= sizeof(int32_t);} else {src = (int16_t*)is->contents+from;dst = (int16_t*)is->contents+to;bytes *= sizeof(int16_t);}memmove(dst,src,bytes);
}

移除一个成员

不同于插入一个成员,移除一个成员时不会改变intset的encoding,尽管移除这个成员之后所有成员的encoding都小于所在intset的encoding。

/* Delete integer from intset */
intset *intsetRemove(intset *is, int64_t value, int *success) {uint8_t valenc = _intsetValueEncoding(value);uint32_t pos;if (success) *success = 0;//valenc不可能大于当前编码,否则value一定不在该intset中if (valenc <= intrev32ifbe(is->encoding) && intsetSearch(is,value,&pos)) {uint32_t len = intrev32ifbe(is->length);/* We know we can delete */if (success) *success = 1;/* Overwrite value with tail and update length */if (pos < (len-1)) intsetMoveTail(is,pos+1,pos);is = intsetResize(is,len-1);//减小内存分配is->length = intrev32ifbe(len-1);//size-1}return is;
}

总结

通过intset底层实现我们可以发现:基于顺序存储的整数集合 执行一些需要用到查询的命令时 其时间复杂度不会是文档里注明O(1),例如:SADD、SREM 操作一个成员时,时间复杂度会是O(logn)。所以当整数集合数据量变大的时候,redis会用dict作为集合的底层实现,将SADD、SREM、SISMEMBER这些命令的时间复杂度降至O(1),当然,这会比intset消耗更多内存。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/463465.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

场景编辑器的草案

Jojoushi场景编辑器 整个脚本的演示和编辑以点击事件为基本的单元&#xff0c;一次点击事件的生命期是&#xff1a;玩家点击一次鼠标到下一次有效的点击鼠标之间的这段时间。 1&#xff0e;显示场景 显示模型存在的场景&#xff0c;可以通过上下左右案件在场景中漫游。 2&…

c++学习书籍推荐《超越C++标准库:Boost库导论》下载

《超越C标准库Boost库导论》不仅介绍了Boost库的功能、使用方法及注意事项&#xff0c;而且还深入讨论了Boost库的设计理念、解决问题的思想和技巧以及待处理的问题。因此&#xff0c;本书是一本了解Boost库并探索其机理的实用手册。 百度云及其他网盘下载地址&#xff1a;点我…

批量替换 MySQL 指定字段中的字符串

批量替换 MySQL 指定字段中的字符串 批量替换 MySQL 指定字段中的字符串是数据库应用中很常见的需求&#xff0c;但是有很多初学者在遇到这种需求时&#xff0c;通常都是用脚本来实现&#xff1b;其实&#xff0c;MySQL 内置的有批量替换语法&#xff0c;效率也会高很多&#x…

WebCombo

原文来自方案网 http://www.fanganwang.com/Product-detail-item-1162.html&#xff0c;欢迎转载。 关键字&#xff1a; WebCombo.NET 是一款基于AJAX技术&#xff0c;处于行业领先地位的Combo box控件。它提供众多高级的数据输入功能及其独特的技术。通过其内置的数据过滤功能…

Redis之压缩列表ziplist

Redis是基于内存的nosql&#xff0c;有些场景下为了节省内存redis会用“时间”换“空间”。 ziplist就是很典型的例子。 ziplist是list键、hash键以及zset键的底层实现之一&#xff08;3.0之后list键已经不直接用ziplist和linkedlist作为底层实现了&#xff0c;取而代之的是qu…

动态链表与静态链表

一. 静态链表 在某些语言中指针是不被支持的,只能使用数组来模拟线性链表的结构.在数组中每个元素不但保存了当前元素的值,还保存了一个”伪指针域”,一般是int类型,用于指向下一个元素的内存地址. #define MAXSIZE 100; typedef struct{ ElemType data; in…

Mysql索引机制B+Tree

1、问题引入    有一个用户表&#xff0c;为了查询的效率&#xff0c;需要基于id去构建索引。构建索引我们需要考虑两个方面的问题&#xff0c;1个是查询的效率&#xff0c;1个是索引数据的存储问题。该表的记录需要支持百万、千万、甚至上亿的数据量&#xff0c;如果将索引…

GLSL学习笔记

GLSL语言内置的变量&#xff0c;包括内置的顶点属性&#xff08;attribute&#xff09;、一致变量&#xff08;uniform&#xff09;、易变变量&#xff08;varying&#xff09;以及常量&#xff08;const&#xff09;&#xff0c;一方面加深印象&#xff0c;另一方面今天的文章…

redis源码之main()函数剖析

今天看了redis的源码之中的main()函数&#xff0c;将大概的流程进行梳理。 在代码中进行了相应的注释&#xff0c;便于阅读者理解&#xff1a; int main(int argc, char **argv) {struct timeval tv;int j;#ifdef REDIS_TESTif (argc 3 && !strcasecmp(argv[1], &qu…

SQL Server 文件规划 -位置规划

数据库设计中&#xff0c;文件规划是相当重要的一个环节。 这部分内容包括文件数目的规划&#xff0c;大小的规划&#xff0c;位置的规划。 本篇介绍的是文件位置的规划&#xff0c;如下图所示 1. 数据文件 有可能的情况下&#xff0c;应该区分经常更新的表与不经常更新的表。分…

const关键字

const关键字const是constant的简写&#xff0c;只要一个变量前面用const来修饰&#xff0c;就意味着该变量里的数据可以被访问&#xff0c;不能被修改。也就是说const意味着“只读”readonly规则&#xff1a;const离谁近&#xff0c;谁就不能被修改&#xff1b;const修饰一个变…

深入理解计算机系统 第十二章 并发编程

如果逻辑控制流在时间上重叠&#xff0c;那么它们就是并发的&#xff08;concurrent&#xff09; 这种常见的现象称为并发&#xff08;concurrency&#xff09;&#xff0c;出现在计算机系统的许多不同层面上。 并发不仅仅局限于内核&#xff0c;它也可以在应用程序中扮演重要角…

Redis源码分析之小型测试框架testhelp.h和redis-check-aof.c日志检测

使用的是redis 3.2版本 test中的文件主要分为以下几个&#xff1a; 1.memtest.c 内存检测 2.redis_benchmark.c 用于redis性能测试的实现&#xff0c;后续会当做单独的一个章节进行分析 3.redis_check_aof.c 用于更新日志检查的实现。 4.redis_check_dump.c 用于本地数据库检查…

VSTS学习和迁移(1) 安装部署

要将开发环境从SVN到VFS中&#xff0c;下个月开始迁移。 先从WebCast中下载文件&#xff0c;看了安装部署部分。以下为部分截图&#xff1a; 一&#xff1a;课程内容 Team Foundation 的逻辑结构与物理结构 Team Foundation 系统要求 Team Foundation 安装实战 Team Founda…

【HNOI2013】数列

题面 题解 设\(\{a_n\}\)为差分数组&#xff0c;可以得到柿子&#xff1a;\[ \begin{aligned} ans & \sum_{a_1 1} ^ m \sum_{a_2 1} ^ m \cdots \sum_{a_{k-1} 1} ^ m (n - \sum_{i 1} ^ {k - 1} a_i) \\ & nm^{k - 1} - \sum_{a_1 1} ^ m \sum_{a_2 1} ^ m \cd…

程序员的艺术:排序算法舞蹈

1、冒泡排序&#xff1a; 2、希尔排序&#xff1a; 3、选择排序&#xff1a; 4&#xff1a;插入排序&#xff1a; 5、快速排序&#xff1a; 6、归并排序&#xff1a; 转载于:https://www.cnblogs.com/jxgxy/archive/2012/08/20/2648210.html

Redis源码分析之内存检测memtest

redis的内存检测会和机器的CPU位数有关&#xff0c;32位或64位会影响后面的一些宏定义参数。首先给出memtest中的API&#xff1a; void memtest_progress_start(char *title, int pass) /* 内存检测加载开始&#xff0c;输出开始的一些图线显示 */ void memtest_progress_end(…

Java Collections Framework - Java集合框架List,Map,Set等全面介绍之概要篇

deng 转载于:https://www.cnblogs.com/jacktu/archive/2009/05/15/1457316.html

C语言 数据结构 树和二叉树

树 1、树&#xff1a;是n节点的有限集。树是n(n>0)个节点的有限集。 n0时成为空树。 在任意一颗非空树中&#xff1a;&#xff08;1&#xff09;有且仅有一个称为根的节点&#xff1b;&#xff08;2&#xff09;当n>0时&#xff0c;其余节点可分为m(m>0)个互不相交的…

Oracle开启关闭归档日志

开启归档日志 shutdown immediate; --关闭数据库 startup mount; --打开数据库 alter database archivelog; --开启归档日志 alter database open; --开启数据库 archive log list; --查看归档日志是否开启 关闭归档日志 shutdown immediate; --关闭数据库 startup mount; …