C 中,字符串是以字符数组的形式表示的,其结尾是空字符‘\0’,用以标识这个字符串的结束(null-terminated)。这样做的好处是字符串的表示灵活,但获取其长度的操作strlen()的时间复杂度是O(N),这样线性的效率对于频繁的字符串操作是不利的,所以Redis自己创建了一种字符串类型用以标识字符串,这就是SDS。
首先,SDS的定义位于SDS.h的sdshdr struct中。
struct sdshdr {
// buf 中已占用空间的长度
int len;
// buf 中剩余可用空间的长度
int free;
// 数据空间
char buf[];
};
这里要注意 结构体中声明的char buf[] 是一个长度为零的数组,这里在结构体中起到的是占位作用,于是sizeof(sdshdr)是不包括char数组空间的,在本人的环境下,sizeof(sdshdr)是8个字节。在源码中会频繁的使用一个指向char数组的pointer减去sizeof(sdshdr)来表示结构体的起始位置。注意,堆是向高地址生长的。所以是数组位于结构体起始位置上方。
SDS的长度不再由‘\0’来表示而是通过sdshdr.len来表示,这样取字符串长度的时间由O(N)下降到了O(1).SDS在buf数组的末尾仍然引入了‘/0’,为的是兼容一些C的字符串操作库函数。len表示的是字符串有效长度,语义与STRLEN类似。通过不再使用NULL-TERMINZTED String,Redis做到了其字符串是二进制安全的(SDS中间可以存在空字符‘\0’).通过len我们知道其长度在哪结束。
还值得一提的是SDS的内存分配算法。因为在堆上分配内存通常是一个耗时间的系统调用,所以在设计时Redis通过预分配和惰性删除的手段尽量减少在堆上分配内存的次数以提高速度。SDS的内存分配算法主要是在其sdsMakeRoomFor中实现的。
预分配是指库为SDS额外预先分配了空间,而惰性删除是指在对于SDS做出减少长度的操作时并不立刻清空这部分减少的内存,而是将这部分多余出来的内存用free表示以供后面使用
sds sdsMakeRoomFor(sds s, size_t addlen) {
struct sdshdr *sh, *newsh;
// 获取 s 目前的空余空间长度
size_t free = sdsavail(s);
size_t len, newlen;
// s 目前的空余空间已经足够,无须再进行扩展,直接返回
if (free >= addlen) return s;
// 获取 s 目前已占用空间的长度
len = sdslen(s);
sh = (void*) (s-(sizeof(struct sdshdr)));
// s 最少需要的长度
newlen = (len+addlen);
// 根据新长度,为 s 分配新空间所需的大小
if (newlen < SDS_MAX_PREALLOC)
// 如果新长度小于 SDS_MAX_PREALLOC
// 那么为它分配两倍于所需长度的空间
newlen *= 2;
else
// 否则,分配长度为目前长度加上 SDS_MAX_PREALLOC
newlen += SDS_MAX_PREALLOC;
// T = O(N)
newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);
// 内存不足,分配失败,返回
if (newsh == NULL) return NULL;
// 更新 sds 的空余长度
newsh->free = newlen - len;
// 返回 sds
return newsh->buf;
}
如果新长度小于 SDS_MAX_PREALLOC 那么为它分配两倍于所需长度的空间,否则,分配长度为目前长度加上 SDS_MAX_PREALLOC。
SDS_MAX_PREALLOC是一个可以调整的值,默认是1MB。
通过分配 现有长度两倍或者现有长度加 SDS_MAX_PREALLOC,当下次再次进行分配是可以从free中直接分配空余空间而不再系统调用分配节省了时间。