什么是整数集合?
整数集合(intset)是Redis用于保存整数值的集合抽象数据结构, 它可以保存类型为int16_t、int32_t或者int64_t的整数值,并且保证集合中不会出现重复元素。
其结构如下:
typedef struct intset {
// 编码方式
uint32_t encoding;
// 集合包含的元素数量
uint32_t length;
// 保存元素的数组
int8_t contents[];
} intset;
其中contents数组是整数集合的底层实现:集合中的每一个元素都是一个数组项,各个项按照从小到大有序排列,并且不包括任何重复的项。
length记录了整数集合包含的元素数量,也是contents数组的长度
虽然 intset 结构将 contents 属性声明为 int8_t 类型的数组, 但实际上 contents 数组并不保存任何 int8_t 类型的值 —— contents 数组的真正类型取决于 encoding 属性的值:
- 如果
encoding属性的值为INTSET_ENC_INT16, 那么contents就是一个int16_t类型的数组, 数组里的每个项都是一个int16_t类型的整数值 (最小值为-32,768,最大值为32,767)。 - 如果
encoding属性的值为INTSET_ENC_INT32, 那么contents就是一个int32_t类型的数组, 数组里的每个项都是一个int32_t类型的整数值 (最小值为-2,147,483,648,最大值为2,147,483,647)。 - 如果
encoding属性的值为INTSET_ENC_INT64, 那么contents就是一个int64_t类型的数组, 数组里的每个项都是一个int64_t类型的整数值 (最小值为-9,223,372,036,854,775,808,最大值为9,223,372,036,854,775,807)。
下面给出一个整数集合示例:
encoding属性的值为INTSET_ENC_INT16, 表示整数集合的底层实现为int16_t类型的数组, 而集合保存的都是int16_t类型的整数值。length属性的值为5, 表示整数集合包含五个元素。contents数组按从小到大的顺序保存着集合中的五个元素。- 因为每个集合元素都是
int16_t类型的整数值, 所以contents数组的大小等于sizeof(int16_t) * 5 = 16 * 5 = 80位。
类型升级
如果将一个新元素放入整数集合,且新元素的类型比集合中所有的类型都要长,那么整数集合需要先进行升级,才能将新元素添加到整数集合中。
升级的步骤如下:
- 根据新元素的类型, 扩展整数集合底层数组的空间大小, 并为新元素分配空间。
- 将底层数组现有的所有元素都转换成与新元素相同的类型, 并将类型转换后的元素放置到正确的位上, 而且在放置元素的过程中, 需要继续维持底层数组的有序性质不变。
- 将新元素添加到底层数组里面。
下面给出一个例子
假设现在有一个 INTSET_ENC_INT16 编码的整数集合, 集合中包含三个 int16_t 类型的元素
假设我们要将类型为
int32_t 的整数值 65535 添加到整数集合里面, 因为 65535 的类型 int32_t 比整数集合当前所有元素的类型都要长, 所以在将 65535 添加到整数集合之前, 程序需要先对整数集合进行升级。
根据新类型的长度, 以及集合元素的数量(包括要添加的新元素在内), 对底层数组进行空间重分配。
类型升级的好处
提高灵活性
因为 C 语言是静态类型语言, 为了避免类型错误, 我们通常不会将两种不同类型的值放在同一个数据结构里面。
比如说, 我们一般只使用 int16_t 类型的数组来保存 int16_t 类型的值, 只使用 int32_t 类型的数组来保存 int32_t 类型的值, 诸如此类。
但是, 因为整数集合可以通过自动升级底层数组来适应新元素, 所以我们可以随意地将 int16_t 、 int32_t 或者 int64_t 类型的整数添加到集合中, 而不必担心出现类型错误, 这种做法非常灵活。
节约内存
要让一个数组可以同时保存 int16_t 、 int32_t 、 int64_t 三种类型的值, 最简单的做法就是直接使用 int64_t 类型的数组作为整数集合的底层实现。 不过这样一来, 即使添加到整数集合里面的都是 int16_t 类型或者 int32_t 类型的值, 数组都需要使用 int64_t 类型的空间去保存它们, 从而出现浪费内存的情况。
而整数集合现在的做法既可以让集合能同时保存三种不同类型的值, 又可以确保升级操作只会在有需要的时候进行, 这可以尽量节省内存。