redis并没有直接使用C语言传统的字符串表示,而是自己构建了一种名为简单动态字符串(simple dynamic string,SDS)的抽象类型,并将SDS用作Redis的默认字符串表示。
struct sdshdr {
// buf 中已占用空间的长度
int len;
// buf 中剩余可用空间的长度
int free;
// 数据空间,char类型的数组,以空字符'\0'结尾,空字符不计算在SDS中的len属性里
char buf[];
};
SDS与C字符串的区别: 1.SDS中len属性记录了SDS的长度,所以获取一个SDS长度的复杂度仅为O(1),而为了获取C字符串的长度必须对C字符串进行遍历,直到遇到代表字符串结尾的空字符串为止,复杂度为O(N)。 2.SDS的空间分配策略完全杜绝了发生缓冲溢出的可能性:当SDS API需要对SDS进行修改时,API会首先检查SDS的空间是否满足修改所需要的要求,如果不满足的话,API会自动将SDS的空间扩展至执行修改所需的大小,然后才执行实际的修改操作,所以使用SDS既不需要手动修改SDS的空间大小,也不会出现缓冲区溢出问题。 3.减少修改字符串带来的内存重分配次数 3.1空间预分配 - 如果对SDS进行修改之后,SDS的长度将小于1MB,那么将程序分配两倍所需长度的空间。 - 如果对SDS进行修改之后,SDS的长度将大于等于1MB,那么程序会分配所需长度加上SDS_MAX_PREALLOC(默认为1M)。 通过空间预分配策略,Redis可以减少连续执行字符串增长操作所需的内存重分配次数。
3.2 惰性空间释放 - 惰性空间释放用于优化SDS的字符串缩短操作:当SDS的API需要缩短SDS保存的字符串时,程序并不会立即使用内存分配来回收缩短后多出来的字节,而是使用free属性将这些字节的数量记录起来,并等待将来使用。 4.二进制安全 C字符串中的字符必须符合某种编码(如ASCII),并且除了字符串的末的尾之外,字符串里不能包含空字符,否则会被认为是字符串结尾,这些限制使得C字符串只能保存文本数据,而不能保存像图片、音频、视频、压缩文件这样的二进制数据。SDS的API都是二进制安全的,所有SDS API都会以处理二进制的方式处理SDS存放在buf数组里的数据,程序不会对其中的数据做任何限制、过滤、或者假设,数据在写入的时候是什么样的,它被读取时就是什么样的。 C字符串与SDS之间的区别
C字符串
SDS
获取字符串长度的复杂度为O(N)
获取字符串长度的复杂度为O(1)
API不安全,可能会造成缓冲区溢出
API是安全的,不会造成缓冲区溢出
修改字符串长度N次必然需要执行N次内存分配
修改字符串长度N次最多需要执行N次内存分配
只能保存文本
可以保存文本或者二进制数据
可以使用所有
可以使用部分
// 创建一个包含给定C字符串的SDS
sds sdsnewlen(const void *init, size_t initlen) {
struct sdshdr *sh;
// 根据是否有初始化内容,选择适当的内存分配方式
// T = O(N)
if (init) {
// zmalloc 不初始化所分配的内存
sh = zmalloc(sizeof(struct sdshdr)+initlen+1);
} else {
// zcalloc 将分配的内存全部初始化为 0
sh = zcalloc(sizeof(struct sdshdr)+initlen+1);
}
// 内存分配失败,返回
if (sh == NULL) return NULL;
// 设置初始化长度
sh->len = initlen;
// 新 sds 不预留任何空间
sh->free = 0;
// 如果有指定初始化内容,将它们复制到 sdshdr 的 buf 中
// T = O(N)
if (initlen && init)
memcpy(sh->buf, init, initlen);
// 以 \0 结尾
sh->buf[initlen] = '\0';
// 返回 buf 部分,而不是整个 sdshdr
return (char*)sh->buf;
}
// 创建一个包含给定C字符串的SDS,在sdsnewlen基础上添加的对char *init的判断
sds sdsnew(const char *init) {
size_t initlen = (init == NULL) ? 0 : strlen(init);
return sdsnewlen(init, initlen);
}
/*
* 返回 sds 实际保存的字符串的长度
*
* T = O(1)
*/
// 因为创建SDS时返回的是指向buf部分的指针,所以通过指针运算取得指向sdshdr的指针
static inline size_t sdslen(const sds s) {
struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
return sh->len;
}
/*
* 返回 sds 可用空间的长度
*
* T = O(1)
*/
static inline size_t sdsavail(const sds s) {
struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
return sh->free;
}
// 创建并返回一个只保存了空字符串""的sds
sds sdsempty(void) {
return sdsnewlen("",0);
}
// 复制给定sds的副本
sds sdsdup(const sds s) {
return sdsnewlen(s, sdslen(s));
}
// 释放sds
void sdsfree(sds s) {
if (s == NULL) return;
zfree(s-sizeof(struct sdshdr));
}
// 清空SDS保存的字符串为空字符串,并没有释放内存
void sdsclear(sds s) {
// 取出 sdshdr
struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));
// 重新计算属性
sh->free += sh->len;
sh->len = 0;
// 将结束符放到最前面(相当于惰性地删除 buf 中的内容)
sh->buf[0] = '\0';
}
/* 扩展sds大小,先判断空余长度是否大于需要扩展的长度,如果是直接返回sds。然后再判断,如果对SDS进行修改之后,SDS的长度将小于1MB,那么将程序分配两倍所需长度的空间。
如果对SDS进行修改之后,SDS的长度将大于等于1MB,那么程序会分配所需长度加上SDS_MAX_PREALLOC(默认为1M)
*/
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中的空闲空间
sds sdsRemoveFreeSpace(sds s) {
struct sdshdr *sh;
sh = (void*) (s-(sizeof(struct sdshdr)));
// 进行内存重分配,让 buf 的长度仅仅足够保存字符串内容
// T = O(N)
sh = zrealloc(sh, sizeof(struct sdshdr)+sh->len+1);
// 空余空间为 0
sh->free = 0;
return sh->buf;
}
// 返回给定sds分配的内存字节数
size_t sdsAllocSize(sds s) {
struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));
return sizeof(*sh)+sh->len+sh->free+1;
}
// 增加sds的长度,缩减空余空间,并将 \0 放到新字符串的尾端
void sdsIncrLen(sds s, int incr) {
struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));
// 确保 sds 空间足够
assert(sh->free >= incr);
// 更新属性
sh->len += incr;
sh->free -= incr;
// 这个 assert 其实可以忽略
// 因为前一个 assert 已经确保 sh->free - incr >= 0 了
assert(sh->free >= 0);
// 放置新的结尾符号
s[sh->len] = '\0';
}
// 将长度为 len 的字符串 t 追加到 sds 的字符串末尾
sds sdscatlen(sds s, const void *t, size_t len) {
struct sdshdr *sh;
// 原有字符串长度
size_t curlen = sdslen(s);
// 扩展 sds 空间
// T = O(N)
s = sdsMakeRoomFor(s,len);
// 内存不足?直接返回
if (s == NULL) return NULL;
// 复制 t 中的内容到字符串后部
// T = O(N)
sh = (void*) (s-(sizeof(struct sdshdr)));
memcpy(s+curlen, t, len);
// 更新属性
sh->len = curlen+len;
sh->free = sh->free-len;
// 添加新结尾符号
s[curlen+len] = '\0';
// 返回新 sds
return s;
}
// 将给定字符串 t 追加到 sds 的末尾
sds sdscat(sds s, const char *t) {
return sdscatlen(s, t, strlen(t));
}
// 将另一个 sds 追加到一个 sds 的末尾
sds sdscatsds(sds s, const sds t) {
return sdscatlen(s, t, sdslen(t));
}
// 把数组转换为sds
sds sdsjoin(char **argv, int argc, char *sep) {
sds join = sdsempty();
int j;
for (j = 0; j < argc; j++) {
join = sdscat(join, argv[j]);
if (j != argc-1) join = sdscat(join,sep);
}
return join;
}
typedef int int_t; // declares int_t to be an alias for the type int
typedef char char_t, *char_p, (*fp)(void); // declares char_t to be an alias for char
// char_p to be an alias for char*
// fp to be an alias for char(*)(void)
在计算机科学中,内联函数(有时称作在线函数或编译时期展开函数)是一种编程语言结构,用来建议编译器对一些特殊函数进行内联扩展(有时称作在线扩展);也就是说建议编译器将指定的函数体插入并取代每一处调用该函数的地方(上下文),从而节省了每次调用函数带来的额外时间开支。但在选择使用内联函数时,必须在程序占用空间和程序执行效率之间进行权衡,因为过多的比较复杂的函数进行内联扩展将带来很大的存储资源开支。另外还需要特别注意的是对递归函数的内联扩展可能引起部分编译器的无穷编译。内联函数 static inline,使用static修饰符,函数仅在文件内部可见,不会污染命名空间
void *memset( void *dest, int ch, size_t count );
Copies the value ch (after conversion to unsigned char as if by (unsigned char)ch) into each of the first count characters of the object pointed to by dest.
The behavior is undefined if access occurs beyond the end of the dest array. The behavior is undefined if dest is a null pointer
Ref: 1.Redis设计与实现 2.Redis源码