Simple Dynamic Strings(SDS)源码解析和使用说明一
SDS是Redis源码中一个独立的字符串管理库。它是由Redis作者Antirez设计和维护的。一开始,SDS只是Antirez为日常开发而实现的一套字符串库,它被使用在Redis、Disque和Hiredis等作者维护的项目中。但是作者觉得这块功能还是比较独立的,应该让其成为一个独立的库去被使用。于是就开发了第二版的SDS。本文我们要讨论的SDS就会是基于这个版本的。(转载请指明出于breaksoftware的csdn博客)
结构
一般来说,如果我们要设计一个字符串,可能会使用到结构。其中至少要保存一个指向字符串内容的指针,比如
struct string_demo {char* data_ptr;
};
如果我们考虑到strlen计算会随着字符串长度增长而耗费更多时间时,可能还需要给结构体增加一个字符串长度字段
struct string_demo {size_t data_len;char* data_ptr;
};
如果还要考虑到字符串对象的使用安全性,我们可能还需要给该结构增加一个引用计数
struct string_demo {int refrence;size_t data_len;char* data_ptr;
};
对于这样的字符串结构体对象,我们在使用时往往需要使用指针方式才能引用到相应的成员变量。比如我们要使用C语言中strlen计算字符串长度,则需要如下操作
string_demo_ptr->data_len = strlen(string_demo_ptr->data_ptr);
或者我们就需要开发出一套针对我们设计的字符串结构的计算方法
size_t calc_string_demo_len(string_demo* ptr) {……
}
但是无论哪种方式,这种设计都导致C语言中字符串操作函数不能直接操作我们的字符串结构体对象。但SDS库则通过巧妙的设计让我们可以直接使用这些函数操作SDS字符串。我们看下它的定义
typedef char *sds;
SDS字符串sds只是char*的别名,它并没有使用结构体去表示这个对象。这样我们就可以从语法层面保证调用C语言中字符串方法不会报错。然而通过这种写法,我们应该可以想到,sds所指向的内存空间保存就是字符串的内容,且和C语言中字符串内容的格式存在兼容性(没说一致性,因为SDS字符可以存储null,后面我们会做说明)。这样才可以让诸如strlen之类的方法正确执行。
但是,如果SDS字符串结构仅仅如此,那就没有必要通过一篇博文去解释了。SDS字符其实也存在我们上述猜想中的结构,只是它没有让保存字符串内容的指针和结构体混为一体,但是在内存分布上是连续的。这个怎么理解呢?我认为Antirez在设计SDS时,是希望实现一套兼容C语言字符串只读性操作方法(不修改字符串内容)的结构。这样的话就要保证结构是一个char*型的指针,且内容指向字符串内容。但是为了可以更加高效的获取字符串长度以及辅助其他操作,则需要保存这个字符串额外的信息。这些信息总不能在内存中使用(字符串指针地址,额外信息)这样的map结构存储,因为通过指针地址查找额外信息的过程会降低整体效率,且这种割裂式的分布也不利于对象的整体管理。那么就让它的额外信息分布在其内容附近。于是就有分布在内容前还是内容后这两种选择。我们先来探讨分布在内容后,这样的设计会导致每个SDS字符串必须以NULL结尾,这会限制住SDS的承载能力。而且每次要取额外信息都要计算字符串结尾位置,这种计算会随着字符串变长而消耗更多时间。所以这种方案不可取。那么只剩下放在内容前这种方案了,SDS的确也是这么设计的。
+--------+-------------------------------+-----------+
| Header | Binary safe C alike string... | Null term |
+--------+-------------------------------+-----------+|`-> Pointer returned to the user.
有一点我们之前没有注意,SDS字符串最后要以NULL字符结尾。这样的设计可以预防用户设置的字符串内容溢出。
我们看下Header部分是什么样的结构
struct __attribute__ ((__packed__)) sdshdr5 {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之外,其他结构都是相似的。我们先看看sdshdr,它只有flags和buf成员,其中flag空间被充分利用,其第三位保存了SDS字符串的类型:
#define SDS_TYPE_5 0
#define SDS_TYPE_8 1
#define SDS_TYPE_16 2
#define SDS_TYPE_32 3
#define SDS_TYPE_64 4
高5位则保存了字符串的长度,那么可见sdshdr5对应的SDS_TYPE_5类型字符串只能保存原串长度小于等于2^5=32个。
我们再看下结构相对统一的其他头结构。由于不同类型的SDS字符串是为了保存不同长度的内容,所以它们主要区别是成员的类型不同。第一个成员变量len记录的是为buf分配的内存空间已使用的长度;第二个成员变量alloc记录的是为buf分配的内存空间的总长度,当然这长度不包括SDS字符串头和结尾NULL。第三个字符flags只是在第三位保存了SDS字符串类型,而剩下的高五位则没有使用。
由于SDS字符串结构的设计,在我们需要访问头中成员变量时,需要通过sds指针向前回溯一个头结构体的长度,然后通过这个地址去访问。至于回溯多长,则要视该SDS字符串的类型而定,而这个信息就保存在sds指针前一个unsigned char长度的空间中——即flags。以获取SDS字符串内容的长度为例:
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))
#define SDS_TYPE_5_LEN(f) ((f)>>SDS_TYPE_BITS)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;
}
SDS_HDR宏在代码中出现非常多,因为通过它我们可以找到并转换SDS字符串头结构地址。
创建SDS字符串
我们可以通过下面四个方法进行SDS字符串的创建
sds sdsnewlen(const void *init, size_t initlen);
sds sdsnew(const char *init);
sds sdsempty(void);
sds sdsdup(const sds s);
sdsnew方法要求传入一个NULL结尾的字符串内存地址,其底层调用的是sdsnewlen方法:
sds sdsnew(const char *init) {size_t initlen = (init == NULL) ? 0 : strlen(init);return sdsnewlen(init, initlen);
}
sdsempty方法创建了一个空的SDS字符串,其底层也是调用了sdsnewlen:
sds sdsempty(void) {return sdsnewlen("",0);
}
sdsdup方法用于复制一个SDS字符串对象,其底层还是使用sdsnewlen去实现的:
sds sdsdup(const sds s) {return sdsnewlen(s, sdslen(s));
}
现在我们重点关注下sdsnewlen方法的实现。
首先,我们需要通过传入的长度确定创建什么类型的SDS字符串
sds sdsnewlen(const void *init, size_t initlen) {void *sh;sds s;char type = sdsReqType(initlen);/* 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;
我们看下选择类型的原则:
static inline char sdsReqType(size_t string_size) {if (string_size < 1<<5)return SDS_TYPE_5;if (string_size < 1<<8)return SDS_TYPE_8;if (string_size < 1<<16)return SDS_TYPE_16;if (string_size < 1ll<<32)return SDS_TYPE_32;return SDS_TYPE_64;
}
如果要求创建一个空串,作者认为一般创建的空串,在未来都是用于填充数据的。所以此时创建一个承载内容长度小于32的类型是不合适的,于是采用SDS_TYPE_8类型。那么只有在字符串内容在1到32之间的情况下才会创建SDS_TYPE_5类型的字符串。
接下来要计算相应类型的头长度,并且根据头长度、字符串长度等申请一段空间
int hdrlen = sdsHdrSize(type);unsigned char *fp; /* flags pointer. */sh = s_malloc(hdrlen+initlen+1);
计算长度时最后加上是因为SDS字符串的整体结构要求以NULL结尾。
如果要创建的是空串,则要将申请的内存都置空
if (!init)memset(sh, 0, hdrlen+initlen+1);if (sh == NULL) return NULL;
下一步,我们需要获取sds地址和SDS字符串头地址
s = (char*)sh+hdrlen;fp = ((unsigned char*)s)-1;
然后根据类型,我们需要向SDS字符串头结构中填写相应值,我们只看下SDS_TYPE_5和SDS_TYPE_8的设置
switch(type) {case SDS_TYPE_5: {*fp = type | (initlen << SDS_TYPE_BITS);break;}case SDS_TYPE_8: {SDS_HDR_VAR(8,s);sh->len = initlen;sh->alloc = initlen;*fp = type;break;}
可以见得,在最初创建SDS字符串时,alloc大小和len大小是一样的。它们产生差别是在之后介绍的字符串连接时。
最后我们根据需要创建的字符串是否有内容而将相关数据复制到内存中,并让内存最后一位为NULL
}if (initlen && init)memcpy(s, init, initlen);s[initlen] = '\0';return s;
}
我们看下这些方法的使用样例
sds mystring = sdsnew("Hello World!");
printf("%s\n", mystring);
sdsfree(mystring);output> Hello World!
char buf[3];
sds mystring;buf[0] = 'A';
buf[1] = 'B';
buf[2] = 'C';
mystring = sdsnewlen(buf,3);
printf("%s of len %d\n", mystring, (int) sdslen(mystring));output> ABC of len 3
sds mystring = sdsempty();
printf("%d\n", (int) sdslen(mystring));output> 0
sds s1, s2;s1 = sdsnew("Hello");
s2 = sdsdup(s1);
printf("%s %s\n", s1, s2);output> Hello Hello
获取SDS字符串长度
可以通过下面的方法获取SDS字符串的长度
size_t sdslen(const sds s);
在之前,我们已经看了该函数的实现了。它只是返回SDS字符串头中的len字段值。这样设计有两个好处:
- 相较于C语言中strlen,sdslen计算SDS字符串的长度的时间是固定的。我们知道strlen通过遍历内存,一直找到NULL才能计算出字符串长度。而SDS字符串长度在被改变时已经被计算好了,它被保存在字符串头结构中,这样每次获取时只要通过固定的地址偏移便可以拿到。
- 它是二进制安全的。因为不依赖于NULL计算长度,所以NULL字符不再那么特殊了。SDS字符串中可以包含若干个NULL。
但是有个东西需要注意下。虽然我们可以使用C语言中的只读性方法访问SDS字符串,但是由于SDS字符串内容中可以包含NULL,而C语言中字符串以NULL结尾,则会在混用时遇到一些的现象,如:
sds s = sdsnewlen("A\0\0B",4);
printf("%d\n", (int) sdslen(s));output> 4
释放字符串
由于SDS字符串的所有空间都是在堆上分配的,所以在不使用时我们需要释放它。
void sdsfree(sds s);
我们看下其实现
void sdsfree(sds s) {if (s == NULL) return;s_free((char*)s-sdsHdrSize(s[-1]));
}
该函数一开始时判断了传入的是不是NULL,所以我们在调用sdsfree前就不需要判断入参是否为空了。然后通过一系列位移计算出SDS字符串头的起始地址,它就是之前在sdsnewlen中通过malloc在堆上分配的空间地址,于是我们要使用free方法释放它。
在之前我们介绍过,创建的空SDS字符串其实也是占用了一定的堆上空间,所以对空SDS字符串也要使用sdsfree去释放,否则会造成内存泄漏。最后我们看下使用的方法:
if (string) sdsfree(string); /* Not needed. */
sdsfree(string); /* Same effect but simpler. */
相关文章:
“不会Linux,到底有多危险?”骨灰级成程序员:基本等于自废武功!
说起程序员的必备技能,我想大家都可以说很多,比如:算法、数据结构、数学、编程语言等等。对于程序员来讲,这些底层能力固然重要,但是,工具同样也是如此,比如常被大家所忽视的:Linux。…

“Uncaught TypeError: string is not a function”
http://www.cnblogs.com/haitao-fan/archive/2013/11/08/3414678.html 今天在js中写了一个方法叫做search(),然后点击按钮的时候提示: “Uncaught TypeError: string is not a function” 百思不得其解啊,我的js木有问题啊啊.... 后来才发现酱…

关于Nikon Ai AF 28mm F1.4D遮光罩的问题
-- 好不容易找到百变妖,确实比较妖!!遮光罩不好找,原厂推荐的HK-7基本属于古董中的古董。 爬文很久,终于找到一篇国外的介绍,说可以用HK-4代替,比HK-7效果更好,而且可以用85mm 1.4D-…

Simple Dynamic Strings(SDS)源码解析和使用说明二
在《Simple Dynamic Strings(SDS)源码解析和使用说明一》文中,我们分析了SDS库中数据的基本结构和创建、释放等方法。本文将介绍其一些其他方法及实现。(转载请指明出于breaksoftware的csdn博客) 字符串连接 SDS库提供下面两种方法进行字符串…
亚马逊机器学习服务:深入研究AWS SageMaker
作者 | Manish Manalath译者 | Shawn编辑 | Carol出品 | AI科技大本营(ID: rgznai100) 机器学习是一个从数据中发现模式的强大概念。但是,如果您尝试过从零开始构建机器模型,那么您一定知道设计一个可扩展的机器学习工作流是多大的…

Java Timer 定时器的使用
一、延时执行首先,我们定义一个类,给它取个名字叫TimeTask,我们的定时任务,就在这个类的main函数里执行。 代码如下:package test;import java.util.Timer;public class TimeTaskTest { public static void main(Str…
Redis源码解析——前言
今天开启Redis源码的阅读之旅。对于一些没有接触过开源代码分析的同学来说,可能这是一件很麻烦的事。但是我总觉得做一件事,不管有多大多难,我们首先要在战略上蔑视它,但是要在战术上重视它。除了一些高大上的技术,我们…

asp.net客户端脚本验证小技巧
通用的客户端脚本验证 Code//验证客户端function checkclient() { var list document.all; for(var i0 ;i<list.length; i) { var h list[i].hint; if(h ! null && h ! "") { if(list[i].isdrop"…
5个可以帮助你提高工作效率的新AI工具
作者 | Kyrylo Lyzanets译者 | 火火酱编辑 | Carol出品 | AI科技大本营(ID: rgznai100) 毫无意义的新闻、故事和活动会占用你每天多少的工作时间?假如你是一名需要高绩效的高管或专业人士,如果在工作中可以不分心,那你…

Centos6.5更换163源 epel源
想必大家都遇到过,安装新的centos系统,使用yum去安装软件的时候,要么找不到,要么慢的让人发疯。网上其实办法很多,直接更换163源就ok,但是基本所有的文章都是直接wget下163的源,但是不知道为什么…
图模型+Bert香不香?完全基于注意力机制的图表征学习模型Graph-Bert
作者 | Jiawei Zhang、Haopeng Zhang、Congying Xia、Li Sun译者 | 凯隐编辑 | Jane出品 | AI科技大本营(ID:rgznai100)【导读】本文提出了一种全新的图神经网络 Graph-Bert,仅仅基于 Attention 机制而不依赖任何类卷积或聚合操作…

闭关纪要17.Google app engine的简单应用
在上面用了十一篇博客的文章详细的介绍了,Step1账户登录系统之后,从现在开始,继续写闭关纪要,因为Step1账户登录系统也是闭关工作的一部分,因此保留序号,这篇纪要在上次的闭关纪要5.WML,UTF-8,BOM,签名及其…
Redis源码解析——内存管理
在《Redis源码解析——源码工程结构》一文中,我们介绍了Redis可能会根据环境或用户指定选择不同的内存管理库。在linux系统中,Redis默认使用jemalloc库。当然用户可以指定使用tcmalloc或者libc的原生内存管理库。本文介绍的内容是在这些库的基础上&#…

poj_2479 动态规划
题目大意 给定一列数,从中选择两个不相交的连续子段,求这两个连续子段和的最大值。 题目分析 典型的M子段和的问题,使用动态规划的方法来解决。 f[i][j] 表示将A[1...i] 划分为j个不相交连续子串,且A[j]属于第i个子串,…
Redis源码解析——字典结构
C语言中有标准的字典库,我们可以通过pair(key,value)的形式存储数据。但是C语言中没有这种的库,于是就需要自己实现。本文讲解的就是Redis源码中的字典库的实现方法。(转载请指明出于breaksoftware的csdn博客) 一般情况下…
十步,教你把Python运行速度提升 30%
作者 | Martin Heinz译者 | 陆离编辑 | Jane出品 | AI科技大本营(ID:rgznai100)【导读】一直以来,诟病 Python语言的人经常说,他们不想使用的一个原因是 Python 的速度太慢了。不管使用哪一种编程语言,程序…

字符串转换成NSDate类型的 为nil解决方法
方法一 通过下列函数来解决 但是得到的日期会改变 修改方法fix- (NSDate *)timeForString:(NSString *)string {NSMutableString *timeString [[NSMutableString alloc] initWithString:string]; [timeString setString:[timeString stringByReplacingOccurrence…
Redis源码解析——字典基本操作
有了《Redis源码解析——字典结构》的基础,我们便可以对dict的实现进行展开分析。(转载请指明出于breaksoftware的csdn博客) 创建字典 一般字典创建时,都是没有数据的,但是字典类型需要确定,所以我们看到R…

[转]控制 Cookie 的作用范围
默认时,网站的所有 Cookies 都一起被存储在客户端,并且所有 Cookies 连同网站的任何请求一起被发送到服务器。换句话说,网站中的每个页面都能够为网站获取所有的 Cookies。但是,你能够通过两个方式来设置 Cookies 的作用范围&…
强化学习70年演进:从精确动态规划到基于模型
作者 | Nathan Lambert译者 | 泓礼编辑 | 夕颜出品 | AI科技大本营(ID: rgznai100)【导读】这是一份帮你了解强化学习算法本质的资源,无需浏览大量文档,没有一条公式,非常适合学生和研究人员阅读。作为强化学习研究人员…

Android ActionBar相关
1.Android 5.0 删除ActionBar下面的阴影 于Android 5.0假设你发现的ActionBar下面出现了阴影,例如,下面的设置,以消除阴影: getActionBar().setElevation(0); Android 5.0之前能够用以下代码消除阴影: <item name&q…
Redis源码解析——字典遍历
之前两篇博文讲解了字典库的基础,本文将讲解其遍历操作。之所以将遍历操作独立成一文来讲,是因为其中的内容和之前的基本操作还是有区别的。特别是高级遍历一节介绍的内容,充满了精妙设计的算法智慧。(转载请指明出于breaksoftwar…
开发者在行动!中国防疫开源项目登上GitHub TOP榜
整理 | 唐小引出品 | CSDN(ID:CSDNnews)【导读】用开发者们的方式支援这场没有硝烟的战争!截止北京时间 1 月 28 日下午 15:47,全国确诊新型冠状病毒的数字已经到达了 4586 例,疑似高达 6973 例,…

像童话一样学习OSPF原理
可以把整个网络(一个自治系统AS)看成一个王国,这个王国可以分成几个 区(area),现在我们来看看区域内的某一个人(你所在的机器root)是怎样得到一张 世界地图(routing table)的。 首先,你得跟你周围的人(…

队列——PowerShell版
继续读啊哈磊《啊哈!算法》感悟系列——队列 地铁售票处排队,先来的人先到队首先买完先走,后来的人排在队尾等候后买完后走。 想买票,必须排在队尾;买完票,只能从队首离开。 这种先进先出(First…

Redis源码解析——双向链表
相对于之前介绍的字典和SDS字符串库,Redis的双向链表库则是非常标准的、教科书般简单的库。但是作为Redis源码的一部分,我决定还是要讲一讲的。(转载请指明出于breaksoftware的csdn博客) 基本结构 首先我们看链表元素的结构。因为…

12月第三周安全要闻回顾:浏览器安全不容忽视,SSL弱点影响网站安全
本周(081215至081221)安全方面的新闻众多,主要集中在***与威胁趋势方面。浏览器安全方向波澜起伏,微软推出了针对上周公开的IE7新漏洞的紧急安全补丁,但目前互联网上针对该漏洞的大规模***仍在继续,******的…
GPT2文本生成有问题?这里有些潜在解决思路
作者 | Leo Gao译者 | 凯隐编辑 | 夕颜出品 | AI科技大本营(ID: rgznai100) 【导读】在过去的一年中,人们对文本生成模型的兴趣重新燃起,这在很大程度上要归功于GPT2,它主要展示了使用更大模型、更大数据和更大计算量的…

HTML5学习之二:HTML5中的表单2
(本内容部分节选自《HTML 5从入门到精通》) 对表单的验证 ———————————————————————————————————————————————————————— •1、required属性 required属性主要目的是确保表单控件中的值已填写。在提交时&…
Redis源码解析——有序整数集
有序整数集是Redis源码中一个以大尾(big endian)形式存储,由小到大排列且无重复的整型集合。它存储的类型包括16位、32位和64位的整型数。在介绍这个库的实现前,我们还需要先熟悉下大小尾内存存储机制。(转载请指明出于…