Overview
SDS 全称是 simple dynamic string。Redis 的字符串实现是 SDS,它是一个结构体,但是却可以和 C 标准的 printf 兼容。
5 个 SDS 结构体
一般都觉得字符串就应该只有一个对应的结构体,但是 Redis 中却有 5 个。在这个 issue 之前确实只有一个,但之后就有 5 个了:https://github.com/redis/redis/pull/2509
这篇 issue 的内容概括起来就是,之前的 SDS 有 4GB 大小限制,因为现代 CPU 逐渐变成了 64 位,旧的 SDS 已经不适合用在 64 位机器上了,所以就改进了 SDS,顺便还把内存占用减小了。issue 中测试结果是减少了 16% 内存占用,带来了 25% 的性能牺牲。这个 issue 刚提出的时候,SDS 只有 4 中,后面为了进一步节省内存就多了 sds5 这种类型。以下结构体是 SDS header 的定义。
struct __attribute__ ((__packed__)) sdshdr5 {
// 低三位是类型,高5位表示字符串长度。这里5表示的是字符串最长长度是2^5=32,
// 但是去掉末位的 \0,所以最多只能存储长度为 31 的。
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* used */
uint8_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len; /* used */
uint16_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len; /* used */
uint32_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len; /* used */
uint64_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
他们对应的字符串最大长度分别是:
sdshdr5 | sdshdr8 | sdshdr16 | sdshdr32 | sdshdr64 |
---|---|---|---|---|
2^5-1 | 2^8-1 | 2^16-1 | 2^32-1 | 2^64-1 |
这里 buf 不会占用实际的内存空间。C 语言允许这样的写法,这样可以让实际的字符串和长度信息都在同一段内存里,而不是一个结构体存储了长度和一个字符指针。如果你申请的空间大于 sizeof(header),那么多出来的空间就可以当作 buf 字段的来使用。
实际上用户获取的指针是 buf 字段的指针,也就是 sds.buf 这个指针。
SDS 创建
SDS 创建有以下 API:
sds sdsnewlen(const void *init, size_t initlen);
sds sdstrynewlen(const void *init, size_t initlen);
sds sdsnew(const char *init);
sds sdsempty(void);
sds sdsdup(const sds s);
因为大同小异所以只介绍 sdsnewlen。
sds sdsnewlen(const void *init, size_t initlen) {
return _sdsnewlen(init, initlen, 0);
}
_sdsnewlen
是一个sds内部使用的函数,其声明是:
sds _sdsnewlen(const void *init, size_t initlen, int trymalloc);
trymalloc 标志了 malloc 失败的时候是返回错误值还是直接 abort。
它的流程如下:
- 根据 initlen 确定该 SDS 的类型。如果小于 32 则为 SDS5,256 则 8,以此类推。例外是 0。里面有一行,意思是长度为 0 的字符串常常要追加字符串。
/* Empty strings are usually created in order to append. Use type 8
* since type 5 is not good at this. */
if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
-
分配对应的 header size + initlen + 1。+ 1 是为了留够一个 \0 的空间。
-
修改 header 的 alloc(已经分配给该 sds 的空间大小 - header size),len,修改标志 SDS 类型的 flag 字段。如果是 SDS5,那么就只存储长度到 flag 里面:
*fp = type | (initlen << SDS_TYPE_BITS);
SDS 修改操作
SDS5 基本只会在创建的时候出现,除了 sdsIncrLen 之外,都会把它变成其他的 SDS 类型。
修改操作也是大同小异。仅介绍 cat 和 format 操作。
sdscat
/* Append the specified binary-safe string pointed by 't' of 'len' bytes to the
* end of the specified sds string 's'.
*
* After the call, the passed sds string is no longer valid and all the
* references must be substituted with the new pointer returned by the call. */
sds sdscatlen(sds s, const void *t, size_t len) {
size_t curlen = sdslen(s);
s = sdsMakeRoomFor(s,len);
if (s == NULL) return NULL;
memcpy(s+curlen, t, len);
sdssetlen(s, curlen+len);
s[curlen+len] = '\0';
return s;
}
/* Append the specified null terminated C string to the sds string 's'.
*
* After the call, the passed sds string is no longer valid and all the
* references must be substituted with the new pointer returned by the call. */
sds sdscat(sds s, const char *t) {
return sdscatlen(s, t, strlen(t));
}
cat 调用的 sdsMakeRoomFor 会自动转换 SDS 的类型。它调用的是 sds _sdsMakeRoomFor(sds s, size_t addlen, int greedy)
。greedy 表示给 SDS 分配的空间是刚好容下字符串,还是乘 2:
if (greedy == 1) {
if (newlen < SDS_MAX_PREALLOC)
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC;
}
如果新的 SDS 类型不同,会销毁旧的 SDS(虽然我觉得也不一定非得销毁吧,调用 realloc 说不定也不错)。
if (oldtype==type) {
newsh = s_realloc_usable(sh, hdrlen+newlen+1, &usable);
if (newsh == NULL) return NULL;
s = (char*)newsh+hdrlen;
} else {
/* Since the header size changes, need to move the string forward,
* and can't use realloc */
newsh = s_malloc_usable(hdrlen+newlen+1, &usable);
if (newsh == NULL) return NULL;
memcpy((char*)newsh+hdrlen, s, len+1);
s_free(sh);
s = (char*)newsh+hdrlen;
s[-1] = type;
sdssetlen(s, len);
}
之后就是调用 c 标准的 memcpy,重新设置长度信息。
sdscatprintf
sds sdscatprintf(sds s, const char *fmt, ...) {
va_list ap;
char *t;
va_start(ap, fmt);
t = sdscatvprintf(s,fmt,ap);
va_end(ap);
return t;
}
这里调用了 sdscatvprintf,它进一步调用的是 c 标准的 vsnprintf。这里有猜测字符串长度的操作。简单描述是,sdscatvprintf 有一个栈上的字符数组 staticbuf[1024]
,如果 fmt 长度 * 2 小于它则用这个 staticbuf,否则使用堆上分配的 buf[fmt 的长度 * 2]
。里面的错误处理有两个,都是根据 vsnprintf 的返回值说明编写的(虽然我实在想不出来这输出到字符串,除了空间不够,还能够有什么错误)。
sds sdscatvprintf(sds s, const char *fmt, va_list ap) {
va_list cpy;
...
while(1) {
va_copy(cpy,ap);
bufstrlen = vsnprintf(buf, buflen, fmt, cpy);
va_end(cpy);
// vsnprintf 返回负数代表输出错误
if (bufstrlen < 0) {
if (buf != staticbuf) s_free(buf);
return NULL;
}
// vsnprintf 返回值大于等于buflen代表缓冲区无法装下所有字符
if (((size_t)bufstrlen) >= buflen) {
if (buf != staticbuf) s_free(buf);
buflen = ((size_t)bufstrlen) + 1;
buf = s_malloc(buflen);
if (buf == NULL) return NULL;
continue;
}
// vsnprintf 返回非负数,并且bufstrlen小于buflen,代表成功
break;
}
...
关于为什么要猜测长度而不是手写一个 fmt 解析,可能是非常长的 fmt 极度少见,并且输出长度大于 1024 的非常少见吧。
顺便一提,这种用户自己包装的 printf,如果想要用上格式化检查,redis 里就有一个例子:
sds sdscatprintf(sds s, const char *fmt, ...)
__attribute__((format(printf, 2, 3))); // 检查fmt和可变参数是否匹配。
SDS 类型转换
SDS5 基本只会在创建的时候出现,除了 sdsIncrLen、sdsResize 等以外,都会把它变成其他的 SDS 类型。
SDS 会自动升级类型(比如 SDS8 存不下 2^15 长度的字符串,就得升级为 SDS16),也会自动降级。它会调用 sdsResize。它声明如下:
sds sdsResize(sds s, size_t size, int would_regrow);
would_regrow 的含义是,如果 Resize 之后长度小于 32,那么该类型应当是 SDS8 还是 SDS5?
if (would_regrow) {
/* Don't use type 5, it is not good for strings that are expected to grow back. */
if (type == SDS_TYPE_5) type = SDS_TYPE_8;
}