抑郁症健康,内容丰富有趣,生活中的好帮手!
抑郁症健康 > Redis源码解析1:SDS--完美的C字符串替代

Redis源码解析1:SDS--完美的C字符串替代

时间:2020-09-13 08:39:01

相关推荐

独角兽企业重金招聘Python工程师标准>>>

翻开书第一篇就是介绍sds的,sds代码简单但结构设计非常精妙,通过内存上的操作硬是把C字符串改造成了符合日常操作的sds了。具体的代码细节不做细述,只记录体会,毕竟需要看真正的代码才能明白,而且本文章主要还是给自己一个回忆,自己看过就好,另外就是夹杂一些天马行空的内容帮助理解。

sds.h/.c的代码总结起来有这几点:内存对齐、内存偏移。

sds的好处则有这几点:杜绝溢出、内存预分配、兼容C字符串。

首先说内存对齐和内存偏移,这两者是结合在一起的,或者说内存偏移是基于内存对齐的。sds结构追求性能,所以内存操作是少不了的,但需要面临一个内存对齐的问题,否则内存偏移会出现差错,在sdshdr的定义了如下模式:

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[];};

对于__attribute__ ((__packed__)),这是设置内存对齐的一种方法,效果和#pragma pack一样,可以参考下面的代码:

#include <iostream>using namespace std;struct student{char name[7];int id; char subject[5];} __attribute__ ((aligned(4)));struct teacher{char name[7];int id; char subject[5];} __attribute__ ((packed));#pragma pack(4)struct studentA{char name[7];int id; char subject[5];};#pragma pack()#pragma pack(1)struct teacherA{char name[7];int id; char subject[5];};#pragma pack()int main(int argc, char* argv[]){cout<<"student: "<<sizeof(student)<<endl;cout<<"teacher: "<<sizeof(teacher)<<endl;cout<<"studentA: "<<sizeof(studentA)<<endl;cout<<"teacherA: "<<sizeof(teacherA)<<endl;}

sds中定义的代码主要作用是取消默认的内存对齐,用变量的实际长度来存储,这样整个sds结构中的各个变量都是互相挨着的,才使得用s[-1]来定位flags变量成为可能,否则难免进行更加复杂的偏移计算。内存对齐有两个好处,一个是平台移植,一个是加速CPU读取,本来我还有个疑问:sds取消了内存对齐,难免会影响cpu的访问性能。但仔细看过之后发现多虑了:redis结构体设计的非常好,前面2个变量是len和alloc,都是8的整数倍,在32位或者64位CPU中基本都能一并读取,而flags是一个unsigned char,不管如何都占据一次加载到寄存器的时间,所以个人感觉不会影响CPU的读取性能。

static inline size_t sdslen(const sds s) {unsigned char flags = s[-1];switch(flags&SDS_TYPE_MASK) {case SDS_TYPE_5:return SDS_TYPE_5_LEN(flags);case SDS_TYPE_8:return SDS_HDR(8,s)->len;case SDS_TYPE_16:return SDS_HDR(16,s)->len;case SDS_TYPE_32:return SDS_HDR(32,s)->len;case SDS_TYPE_64:return SDS_HDR(64,s)->len;}return 0;}

之后,大部分的函数操作都会与内存偏移发生联系,例如下面这两个宏在代码中大量使用:

#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))

再来说说内存溢出和预分配:在C字符串中,当发生strcat的时候,需要由程序员保证足够的内存空间,一旦内存不足,就会发生溢出;另外一方面,则需要反复的malloc或者realloc进行内存分配,影响系统的性能。在sds中,则不会发生此类事情,以sdscat函数为例,其内部调用了sdscatlen,而在sdscatlen内部又通过sdsMakeRoomFor进行预分配内存,避免了频繁的内存分配操作:

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;}

再说说sdsMakeRoomFor方法,其实也没啥内涵,就是根据传参进行内存分配,当需要的内存小于1M时,则按照2倍大小分配,否则直接给1M,同时还会重新检查header类型,决定用realloc还是malloc进行操作:

sds sdsMakeRoomFor(sds s, size_t addlen) {void *sh, *newsh;size_t avail = sdsavail(s);size_t len, newlen;char type, oldtype = s[-1] & SDS_TYPE_MASK;int hdrlen;if (avail >= addlen) return s;len = sdslen(s);sh = (char*)s-sdsHdrSize(oldtype);newlen = (len+addlen);if (newlen < SDS_MAX_PREALLOC)//SDS_MAX_PREALLOC大小是1Mnewlen *= 2;elsenewlen += SDS_MAX_PREALLOC;type = sdsReqType(newlen);if (type == SDS_TYPE_5) type = SDS_TYPE_8;hdrlen = sdsHdrSize(type);if (oldtype==type) { //类型不变newsh = s_realloc(sh, hdrlen+newlen+1);if (newsh == NULL) return NULL;s = (char*)newsh+hdrlen;} else { //类型发生变化,导致sds header大小变化newsh = s_malloc(hdrlen+newlen+1);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);}sdssetalloc(s, newlen);return s;}

这边有个地方需要注意,在SDS_HDR_VAR的定义中使用了强转,把void*转换成了sdshdr*,这种转换在C中是可以的,但在C++中就会报错,属于C和C++语言层面的不同。

在3.2.6版本中,sds自带了main函数用来测试,只需要编译时加上-DSDS_TEST_MAIN,基本覆盖了全部函数了,这非常不错,要赞一个。

总的来说,sds非常不错,特别是对于header的设计之处值得学习,不过对于sds本身来说能用的地方应该是非常底层的,要结合具体项目和团队成员背景使用,也需要再度定制过之后才能用到项目中。

我拆分了redis中的sds相关代码,可以到/dodomouse/RedisStructure.git中下载。

参考:

/posts/blog-redis-sds.html

如果觉得《Redis源码解析1:SDS--完美的C字符串替代》对你有帮助,请点赞、收藏,并留下你的观点哦!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。