SDS動態(tài)字符串庫
SDS(Simple Dynamic Strings)是一個C語言字符串庫,設計中增加了從堆上分配內(nèi)存的字符串,來擴充有限的libc字符處理的功能,使得:
-
使用更簡便
-
二進制安全
-
計算更有效率
-
而且仍舊…兼容一般的C字符串功能
它使用另一種設計來實現(xiàn),不用C結(jié)構(gòu)體來表現(xiàn)一個字符串,而是使用一個二進制的前綴(prefix),保存在實際的指向字符串的指針之前,SDS將其返回給用戶。
+--------+-------------------------------+-----------+ | Header | Binary safe C alike string... | Null term | +--------+-------------------------------+-----------+ | Pointer returned to the user.
因為元數(shù)據(jù)作為一個前綴被儲存于實際的返回指針之前,還因為不論字符串的實際內(nèi)容,每個SDS字符串都隱含地在字符串的末尾追加一個空項(null term)。SDS字符串能夠和C字符串一起使用,用戶能夠使用以只讀方式訪問字符串的函數(shù),自由地交替使用它們。
SDS以前是C字符串的庫,是我為自己每天的C編程的需要而開發(fā)的,后來它被遷移到Redis,那里它得到了擴展使用,并且為了適應高性能的操作而修改?,F(xiàn)在它被從Redis分離出來,成了一個獨立的項目。
因為它在Redis里存在了好幾年,SDS不僅提供了能夠簡單操作C字符串的上層函數(shù),還有一系列的底層函數(shù),使得避免因使用上層字符串庫造成的損失而寫出高效能的代碼變?yōu)榭赡堋?/p>
SDS的優(yōu)缺點
通常動態(tài)C字符串庫使用一個定義字符串的結(jié)構(gòu)體來實現(xiàn)。此結(jié)構(gòu)體有一個指針域,由字符串函數(shù)管理,看起來像這樣:
struct yourAverageStringLibrary {
char *buf;
size_t len;
... possibly more fields here ...};
SDS字符串,已經(jīng)提過了,不遵從這樣的模式,而是對在實際返回字符串地址之前的前綴給予一個單獨分配(single allocation)的空間。
相比傳統(tǒng)的方式,這種方法也有其優(yōu)缺點:
缺點#1:
很多函數(shù)以值的形式返回新字符串,由于有時SDS要求創(chuàng)建一個占用更多空間的新字符串,所以大多數(shù)SDS的API調(diào)用像這樣:
s = sdscat(s,"Some more data");
你可以看到s被用來作為sdscat的輸入,但也被設為SDS API調(diào)用返回的值,因為我們不知道此調(diào)用是否會改變了我們傳遞的SDS字符串,還是會重新分配一個新的字符串。忘記將sdscat或者類似函數(shù)的返回值賦回到存有SDS字符串的變量的話,就會引起bug。
缺點#2:
如果一個SDS字符串在你的程序中多個地方共享,當你修改字符串的時候,你必須修改所有的引用。但是,大多數(shù)時候,當你需要共享SDS字符串時,將字符串封裝成一個結(jié)構(gòu)體,并使用一個引用計數(shù)會更好,否則很容易導致內(nèi)存泄露。
優(yōu)點#1:
你無需訪問結(jié)構(gòu)體成員或者調(diào)用一個函數(shù)就可以把SDS字符串傳遞給C函數(shù),就像這樣:
printf("%s\n", sds_string);
而在大多數(shù)其它庫中,這將是像這樣的:
printf("%s\n", string->buf);
或者:
printf("%s\n", getStringPointer(string));
優(yōu)點#2:
直接訪問單個字符。C是個底層的語言,所以在很多程序中這是一個重要的操作。 用SDS字符串來訪問單個字符是很輕松的:
printf("%c %c\n", s[0], s[1]);
用其他庫的話,你最有可能分配string->buf(或者調(diào)用函數(shù)來取得字符串指針)到一個字符指針,并操作此指針。然而,每次你調(diào)用一個函數(shù),可能修改了字符串,其它的庫可能隱式地重新分配緩存,你必須再次取得一個內(nèi)存區(qū)的引用。
優(yōu)點#3:
單次分配有更好的緩存局部性。通常當你訪問一個由使用結(jié)構(gòu)體的字符串庫所創(chuàng)建字符串時,你會有兩塊不同的內(nèi)存分配:用結(jié)構(gòu)體來表現(xiàn)字符串的分配,和實際的內(nèi)存區(qū)里儲存著字符串。過了一段時間后,內(nèi)存重新分配,很可能終結(jié)在一個與結(jié)構(gòu)體本身地址完全不同的內(nèi)存部分。因為現(xiàn)代的程序性能經(jīng)常由緩存未命中次數(shù)所決定的,SDS可以在大工作量下表現(xiàn)得更好。
SDS基礎
SDS字符串的類型只是字符指針char *。 然而,SDS在頭文件里定義了一個sds類型作為char*的別名:你應該用sds類型,來保證你能記住你程序里的一個變量保存了一個SDS字符串而不是C字符串,當然這不是硬性規(guī)定的。
這是你可以寫的能做些事情的最簡單的SDS程序
sds mystring = sdsnew("Hello World!");
printf("%s\n", mystring);
sdsfree(mystring);
output> Hello World!
上面的小程序已經(jīng)展示了一些關(guān)于SDS的重要內(nèi)容:
-
SDS字符串由sdsnew()函數(shù)或者其它類似的函數(shù)創(chuàng)建、從堆上分配內(nèi)存,稍后你將看到。
-
SDS字符串可以被傳遞到printf()像任何其它的C字符串。
-
SDS字符串要求由sdsfree()釋放,因為它們是堆上分配的。
創(chuàng)建SDS字符串
sds sdsnewlen(const void *init, size_t initlen); sds sdsnew(const char *init); sds sdsempty(void);sds sdsdup(const sds s);
創(chuàng)建SDS字符串有很多方法:
-
sdsnew()函數(shù)創(chuàng)建一個以C的空字符結(jié)尾的SDS字符串?。我們已經(jīng)在上述例子中看到它如何工作的。
-
sdsnewlen()函數(shù)類似于sdsnew(),但不同于創(chuàng)建一個輸入是以空字符結(jié)尾的字符串,它取另一個長度的參數(shù)。用這個方法,你可以創(chuàng)建一個二進制數(shù)據(jù)的字符串:
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注意:sdslen的返回值被轉(zhuǎn)換成int,因為它返回一個size_t類型。你可以使用正確的printf標識符而不是類型轉(zhuǎn)換。
-
sdsempty()函數(shù)創(chuàng)建一個空的零長度的字符串:
sds mystring = sdsempty();printf("%d\n", (int) sdslen(mystring)); output> 0 -
sdsup()函數(shù)復制一個已存在的SDS字符串:
sds s1, s2; s1 = sdsnew("Hello");s2 = sdsdup(s1);printf("%s %s\n", s1, s2); output> Hello Hello
獲得字符串長度
size_t sdslen(const sds s);
在上述例子中,我們已經(jīng)用了sdslen()函數(shù)來獲得字符串長度。這個函數(shù)的運作方式類似于libc中的strlen,不同點在于:
-
能以常數(shù)時間運行,因為長度被存在SDS字符串的前綴,所以即使是非常大的字符串,調(diào)用sdslen的花費不昂貴。
-
這個函數(shù)是二進制安全的,就像其它的SDS字符串函數(shù)一樣,所以長度是真正的字符串長度,而不用考慮字符串的內(nèi)容,字符串中間包含空字符也沒有問題。
我們可以運行下面的代碼,來看一個SDS字符串二進制安全的例子:
sds s = sdsnewlen("AB",4);printf("%d\n", (int) sdslen(s));
output> 4
注意,SDS字符串在末尾總是以空字符終結(jié),所以哪怕在樣例中s[4]也會有一個空字符終結(jié),然而用printf打印字符串的話,最終只有“A”被打印出來,因為libc會把SDS字符串當作一般的C字符串那樣處理。
銷毀字符串
void sdsfree(sds s);
要銷毀一個SDS字符串,只需要調(diào)用sdsfree()函數(shù),并將字符串指針作為參數(shù)。但需要注意的是,由sdsempty()創(chuàng)建的空字符串也需要被銷毀,否則它們會造成內(nèi)存泄漏。
如果不是SDS字符串指針,而是NULL指針被傳遞過來,sdsfree()函數(shù)將不執(zhí)行任何操作。因此在調(diào)用它之前你不需要顯式地檢查NULL指針:
if (string) sdsfree(string); /* Not needed. */sdsfree(string); /* Same effect but simpler. */
連接字符串
把字符串和另外的字符連接起來,也許會是你最可能放棄動態(tài)C字符串庫的操作了。SDS提供了不同的函數(shù)來把字符串和已存在的字符串連接起來。
sds sdscatlen(sds s, const void *t, size_t len);sds sdscat(sds s, const char *t);
主要的字符串連接函數(shù)是sdscatlen()和sdscat(),它們基本是一樣的,唯一的區(qū)別是sdscat()沒有一個顯式的長度參數(shù),因為它要求一個以空字符結(jié)尾的字符串。
sds s = sdsempty();s = sdscat(s, "Hello ");s = sdscat(s, "World!");printf("%s\n", s);
output> Hello World!
有時,你需要連接一個SDS字符串到另一個SDS字符串,你不需要指定長度,但同時字符串不需要以空字符結(jié)尾,但可以包含任何二進制數(shù)據(jù)。為此有個特別的函數(shù):
sds sdscatsds(sds s, const sds t);
用法很直接:
sds s1 = sdsnew("aaa");sds s2 = sdsnew("bbb");s1 = sdscatsds(s1,s2);sdsfree(s2);printf("%s\n", s1);
output> aaabbb
有時你不想給字符串添加任何特殊數(shù)據(jù),但你想確定整個字符串至少包含了給定數(shù)量的字節(jié)。
sds sdsgrowzero(sds s, size_t len);
如果現(xiàn)在的字符串長度已經(jīng)是len字節(jié)了的話,sdsgrowzero()函數(shù)不做任何事情;如果不是,它需要用0字節(jié)補齊,把字符串增長到len。
sds s = sdsnew("Hello");s = sdsgrowzero(s,6);s[5] = '!'; /* We are sure this is safe because of sdsgrowzero() */printf("%s\n', s);
output> Hello!
字符串的格式
有個特殊的字符串連接函數(shù),它接收類似printf格式標識符,并且將格式化字符串連接到指定的字符串。
sds sdscatprintf(sds s, const char *fmt, ...) {
樣例:
sds s;int a = 10, b = 20;s = sdsnew("The sum is: ");s = sdscatprintf(s,"%d+%d = %d",a,b,a+b);
經(jīng)常地,你需要直接從printf的格式標識符中創(chuàng)建SDS字符串。因為sdscatprintf()實際上是一個連接字符串的函數(shù),你需要做的只是將你的字符串連接到一個空字符串:
char *name = "Anna";int loc = 2500;sds s;s = sdscatprintf(sdsempty(), "%s wrote %d lines of LISP\n", name, loc);
你可以用sdscatprintf()來把數(shù)字轉(zhuǎn)換成SDS字符串:
int some_integer = 100;sds num = sdscatprintf(sdsempty(),"%d\n", some_integer);
但是這很慢,然而我們有個特殊函數(shù)來提高效率。
數(shù)字到字符串快速轉(zhuǎn)換操作
從一個整數(shù)創(chuàng)建一個SDS字符串在特定類型的程序中可能是一個普通的操作,當你能用sdscatprintf()來完成的時候,會有很大的性能下降,所以SDS提供了一個專用的函數(shù)。
sds sdsfromlonglong(long long value);
用起來像這樣:
sds s = sdsfromlonglong(10000);printf("%d\n", (int) sdslen(s));
output> 5
裁剪字符串和取得區(qū)間
字符串裁剪是一個通常的操作,一系列字符被從字符串的左邊或右邊去除。另一個有用的對字符操作是從一個大字符串中只取出一個區(qū)間。
void sdstrim(sds s, const char *cset);void sdsrange(sds s, int start, int end);
SDS提供dstrim()和sdsrange()函數(shù)來完成這兩個操作。但是,留意,兩個函數(shù)工作方式都不同于大多數(shù)修改SDS字符串的函數(shù),因為它們的返回值為空:基本上那些函數(shù)總是破壞性地修改了傳遞過來的SDS字符串,從來不分配一個新的。因為裁剪和取得區(qū)間從不需要更多的空間:所以這兩個操作可以只從原來的字符串中去除字符。
因為這個行為,這兩個函數(shù)速度很快,并且不涉及到內(nèi)存的重新分配。
這是一個字符串裁剪的例子,里面換行和空格從SDS字符串中被去除了。
sds s = sdsnew(" my string\n\n ");sdstrim(s," \n");printf("-%s-\n",s);
output> -my string-
基本上,sdstrim()把要裁剪的SDS字符串作為第一個參數(shù),并且?guī)в幸粋€以空字符終結(jié)的字符集, 它們會被從字符串的左邊或右邊去除。字符只要不被裁剪字符列表以外的字符隔開,就會被去除:這是為什么“my”和“string”中間的空格在上面的例子中被保留。
取得區(qū)間也類似,但不是取得一組字符,而是在字符串內(nèi)取得表示開始和結(jié)束的索引,由0起始,來取得將被保留的區(qū)間。
sds s = sdsnew("Hello World!");sdsrange(s,1,4);printf("-%s-\n");
output> -ello-
索引可以為負,來指定一個起始于字符串末尾的位置,因此-1表示最后一個字符,-2表示倒數(shù)第二的,以此類推。
sds s = sdsnew("Hello World!");sdsrange(s,6,-1);printf("-%s-\n");sdsrange(s,0,-2);printf("-%s-\n");
output> -World!-output> -World-
當實現(xiàn)網(wǎng)絡服務器處理一個協(xié)議或者發(fā)送消息時,sdsrange()會非常有用。例如,下面的代碼用來實現(xiàn)節(jié)點間的Redis Cluster消息總線的寫處理:
void clusterWriteHandler(..., int fd, void *privdata, ...) {
clusterLink *link = (clusterLink*) privdata;
ssize_t nwritten = write(fd, link->sndbuf, sdslen(link->sndbuf));
if (nwritten <= 0) {
/* Error handling... */
}
sdsrange(link->sndbuf,nwritten,-1);
... more code here ...}
每當我們需要發(fā)送消息的目標節(jié)點的socket是可寫的時候,我們嘗試寫入盡可能多的字節(jié),我們用?sdsrange()從緩沖中移除已經(jīng)發(fā)送的部分。
給發(fā)送到某個集群節(jié)點的新消息排隊的函數(shù)就只是用sdscatlen()來把更多的數(shù)據(jù)放到發(fā)送緩沖中去。
注意,Redis Cluster總線實現(xiàn)了一個二進制的協(xié)議,因為SDS是二進制安全,所以這不會造成問題。所以SDS的目標不僅是為C程序員提供一個高層字符串API,還提供了易于管理的動態(tài)分配緩沖。
字符串復制
最危險、最惡名的C標準庫函數(shù)可能就是strcpy了。所以可能有些有趣的是,在更好設計的動態(tài)字符串庫的環(huán)境中,復制字符串的概念幾乎是無關(guān)緊要的。通常你做的就是創(chuàng)建一個字符串,內(nèi)容由你定,或者根據(jù)需要連接更多內(nèi)容。
然而,SDS以一個有利于高效重要的代碼段的字符串復制函數(shù)為特性。但是我猜它的實用性是有限的,因為這個函數(shù)從來沒在50千行代碼所組成的Redis代碼庫中被調(diào)用。
sds sdscpylen(sds s, const char *t, size_t len);sds sdscpy(sds s, const char *t);
SDS字符串復制函數(shù)叫sdscpylen,像這樣調(diào)用:
s = sdsnew("Hello World!");s = sdscpylen(s,"Hello Superman!",15);
正如你能看到的,這個函數(shù)接收SDS字符串s作為輸入,但也返回一個SDS字符串。這對很多修改字符串的SDS函數(shù)來說很普遍,用這個方法,返回的SDS字符串可能是基于原來的那個修改的,或者一個新分配的(比如舊的SDS字符串沒有足夠空間時)。
sdscpylen只是用由指針和長度參數(shù)傳遞的新數(shù)據(jù),替換掉在舊SDS字符串里的內(nèi)容。還有一個類似的函數(shù)叫sdscpy,不需要長度參數(shù),但是要求帶有空字符終結(jié)的字符串。
你可能會想,為什么SDS庫需要一個字符串復制函數(shù),你可以簡單地從零開始創(chuàng)建一個新的SDS字符串,用新的值,而非復制一個存在的SDS字符串的值。理由是效率:sdsnewlen()總是分配一個新的字符串,而sdscplylen()會盡量重用已存在的字符串,如果有足夠的空間,就用用戶指定的新內(nèi)容,只在必要時分配新的。
引用字符串(Quoted String)
為了給程序用戶提供的輸出,或者為了調(diào)試的目的,將一個可能包含二進制數(shù)據(jù)或者特殊字符的字符串轉(zhuǎn)換成引用的字符串通常是很重要的。這里的引用字符串,意思是程序代碼里的字符串文字上的一般形式。然而今天,這個形式也是著名的串行化格式的一部分,如JSON和CSV,所以它顯然偏離了在程序的源代碼中表現(xiàn)文字上的字符串這一簡單的目標。
下面是一個引用字符串文面的例子:
"\x00Hello World\n"
第一個字節(jié)是一個0字節(jié),當最后一個字節(jié)是一個換行,所以共有兩個非字母的字符在這個字符串里。
SDS使用一個連接函數(shù),把表示輸入字符串的引用字符串,連接到一個已存在的字符串,來達到這個目的。
sds sdscatrepr(sds s, const char *p, size_t len);
scscatrepr()(repr表示representation)遵從通常的SDS字符串函數(shù)規(guī)則,接收一個字符指針和一個長度參數(shù),所以你可以用它來處理SDS字符串,或者一般的使用strlen()作為len參數(shù)的C字符串,或者二進制數(shù)據(jù)。下面是一個使用例子:
sds s1 = sdsnew("abcd");sds s2 = sdsempty();s[1] = 1;s[2] = 2;s[3] = '\n';s2 = sdscatrepr(s2,s1,sdslen(s1));printf("%s\n", s2);
output> "a\x01\x02\n"
這是sdscatrepr()使用的規(guī)則:
-
\和“用backslash引用。
-
能引用特殊字符’\n’, ‘\r’, ‘\t’, ‘\a’以及’\b’
-
所有其他不能通過isprint測試的不可打印字符在\x..格式里被引用,就是backslash后跟x,后跟2位十六進制數(shù)字表示字符串的字節(jié)數(shù)值。
-
這個函數(shù)總是加上初始的和最后的雙引號字符。
有一個SDS函數(shù)能處理逆轉(zhuǎn)換,在下面的語匯單元化(Tokenization)的篇幅里有記述。
語匯單元化(Tokenization)
語匯單元化是一個把大字符串分割成小字符串的過程。在這個特定的例子中,指定另一個字符串作為分隔符來執(zhí)行分割。例如,在下面的字符串,有兩個子字符串被|-|分割符分割:
foo|-|bar|-|zap
一個更常用的由一個字符組成的分割符是逗號:
foo,bar,zap
處理一行內(nèi)容來獲得組成它的子字符串在許多程序中是很有用的,所以SDS提供了一個函數(shù),給定一個字符串和一個分割符,返回一個SDS字符串的數(shù)組。
sds *sdssplitlen(const char *s, int len, const char *sep, int seplen, int *count);void sdsfreesplitres(sds *tokens, int count);
和往常一樣,這個函數(shù)可以處理SDS字符串和普通的C字符串。頭兩個參數(shù)s和len指定了要單元化的字符串,另兩個字符串sep和seplen是在單元化過程中用到的分割符。最后的參數(shù)count是一個整數(shù)指針,會被設為返回的單元(子字符串)的數(shù)目。
返回值是一個在堆上分配的SDS字符串數(shù)組。
sds *tokens;int count, j;
sds line = sdsnew("Hello World!");tokens = sdssplitlen(line,sdslen(line)," ",1,&count);
for (j = 0; j < count; j++)
printf("%s\n", tokens[j]);sdsfreesplitres(tokens,count);
output> Hellooutput> World!
返回的數(shù)組是在堆上分配的,并且數(shù)組的單個元素是普通的SDS字符串。在例子中,你可以通過調(diào)用sdsfreesplitres()釋放所有資源。你也可以選擇用free函數(shù)自行釋放數(shù)組,或者像通常那樣釋放單獨的SDS字符串。
合理的方法是用某種方式將你會重用的數(shù)組元素設置為NULL,并且用sdsfreesplitres()來釋放其余所有的數(shù)組。
面向命令行的單元化
用分割符分割字符串是很有用的操作,但是對于執(zhí)行最常見的涉及到重要的字符串操作,即為程序?qū)崿F(xiàn)命令行接口來說,通常還是不夠的。
這是為什么SDS也提供一個額外的函數(shù),允許你將用戶由鍵盤交互式輸入,或者通過一個文件、網(wǎng)絡或者其他任何方式的參數(shù),分割成單元。
sds *sdssplitargs(const char *line, int *argc);
sdssplitargs函數(shù)返回一個SDS字符串數(shù)組,就像sdssplitlen()一樣。釋放結(jié)構(gòu)的函數(shù)sdsfreesplitres(),也是一樣的。不同在于執(zhí)行單元化的方式。
例如,如果輸入下面一行:
call "Sabrina" and "Mark Smith\n"
函數(shù)會返回下面的標記(token):
-
“call”
-
“Sabrina”
-
“and”
-
“Mark Smith\n”
基本上,不同的標記要被一個或多個空格分割,每一個標記也可以是一個sdscatrepr()可以發(fā)出的相同格式的引用字符串。
字符串結(jié)合(Joining)
有兩個函數(shù)做與單元化相反的工作,將字符串結(jié)合成一個。
sds sdsjoin(char **argv, int argc, char *sep, size_t seplen);sds sdsjoinsds(sds *argv, int argc, const char *sep, size_t seplen);
這兩個函數(shù)取一個長度為argc的字符串數(shù)組,一個分割符及其長度作為輸入,產(chǎn)生一個由所有被輸入分割符分割的輸入字符串所組成的SDS字符串。
sdsjoin()和sdsjoinsds()不同點在于前者接收C空字符終結(jié)的字符串作為輸入,而后者要求所有數(shù)組里的字符串須為SDS字符串。但是也因為這個原因,只有sdsjoinsds()能夠處理二進制數(shù)據(jù)。
char *tokens[3] = {"foo","bar","zap"};sds s = sdsjoin(tokens,3,"|",1);printf("%s\n", s);
output> foo|bar|zap
錯誤處理
所有返回SDS指針的SDS函數(shù),在內(nèi)存不足的情況下,也有可能返回NULL,基本上這是唯一需要你進行檢查的地方。
但是許多現(xiàn)代的C程序處理內(nèi)存不足時,只會簡單地中止程序,所以可能你也會需要通過包裝malloc,直接調(diào)用其他相關(guān)的內(nèi)存分配函數(shù)來處理這種情況。
SDS本質(zhì)和進階用法
在本篇開始時,解釋了SDS字符串是如何被分配的,但是只涉及到保存在返回用戶的指針之前的前綴,被當作一個字符串頭(header)而已,沒有更深入的細節(jié)。為了了解進階的用法,最好挖掘更多SDS的本質(zhì),看看實現(xiàn)它所用到的結(jié)構(gòu)體:
struct sdshdr {
int len;
int free;
char buf[];};
如你所見,這個結(jié)構(gòu)體可能與某個傳統(tǒng)的字符串庫類似,但是結(jié)構(gòu)體的buf域是不同的,因為它不是一個指針,而是一個沒有聲明任何長度的數(shù)組,所以buf實際上指向了緊跟叫free的整數(shù)后的第一個字節(jié)。所以為了創(chuàng)建一個SDS字符串,我們只要分配一片內(nèi)存,其大小為sdshdr結(jié)構(gòu)體加上我們的字符串長度,外加一個額外的字節(jié),這是為了所有SDS字符串硬性需要的空字符。
結(jié)構(gòu)體的len域顯而易見,就是當前的SDS字符串的長度,每當字符串被通過SDS函數(shù)調(diào)用修改時,總是會被重新計算。而free域表示了在當前分配空間中的空閑內(nèi)存的數(shù)量,可以被用來存儲更多的字符。
所以實際的SDS內(nèi)存分布是這個:
+------------+------------------------+-----------+---------------\ | Len | Free | H E L L O W O R L D \n | Null term | Free space \ +------------+------------------------+-----------+---------------\ | Pointer returned to the user.
你可能要問,為什么在字符串末尾會有一些空閑空間,這看上去是浪費。實際上,在一個新的SDS字符串創(chuàng)建后,之后是沒有任何空閑空間的:分配空間小到只需要保存字符串頭、字符串和空終結(jié)符。然而,其他的訪問模式會在末尾創(chuàng)建一些額外的空閑空間,如下面的程序:
s = sdsempty();s = sdscat(s,"foo");s = sdscat(s,"bar");s = sdscat(s,"123");
因為SDS致力于高效,它負擔不起在每次添加新數(shù)據(jù)時,重新分配字符串,因為這會非常的低效,所以會使用每次你擴大字符串時,預分配一些空閑空間。
所使用的預分配算法如下:每次字符串為了保存更多的字節(jié)而被重新分配時,實際進行分配的大小是最小需求的兩倍。例如,如果字符串現(xiàn)在保存了30個字節(jié),我們多連接2個字節(jié),SDS總共會分配64個字節(jié),而非32個。
然而,可進行分配的空間有一個硬性限制,被定義為SDS_MAX_PREALLOC。SDS絕不會分配超過1MB的額外空間(默認的,你可以修改這個默認值)。
縮減字符串
sds sdsRemoveFreeSpace(sds s);size_t sdsAllocSize(sds s);
有時,有一類程序要求使用非常少的內(nèi)存。字符串連接、裁剪、取得區(qū)間后,字符串可能最終會有非常巨大的額外空間。
可以用函數(shù)sdsRemoveFreeSpace()改變字符串大小,回到可以保存現(xiàn)在內(nèi)容的最小尺寸。
s = sdsRemoveFreeSpace(s);
也可以用另一個函數(shù),來取得給定字符串的總的分配空間大小,叫做sdsAllocSize()。
sds s = sdsnew("Ladies and gentlemen");s = sdscat(s,"... welcome to the C language.");printf("%d\n", (int) sdsAllocSize(s));s = sdsRemoveFreeSpace(s);printf("%d\n", (int) sdsAllocSize(s));
output> 109output> 59
注意:SDS底層API使用cammelCase,這可以警告你,你在玩火。
手動修改SDS字符串
void sdsupdatelen(sds s);
有時你會想手動修改一個SDS字符串,而不是用SDS函數(shù)。在下面的例子中,我們隱式地修改字符串的長度,當然,我們還想要邏輯上的長度來表示以空字符終結(jié)的C字符串。
函數(shù)sdsupdatelen()正好做了那些工作,把指定字符串的內(nèi)部長度信息更新成通過strlen得到的長度。
sds s = sdsnew("foobar");s[2] = '';printf("%d\n", sdslen(s));sdsupdatelen(s);printf("%d\n", sdslen(s));
output> 6output> 2
共享SDS字符串
如果你在寫一個程序,在其中,不同的數(shù)據(jù)結(jié)構(gòu)間分享同一個SDS字符串會有好處的話,強烈建議,把SDS字符串封裝到一個結(jié)構(gòu)體中,結(jié)構(gòu)體中包含記錄引用字符串的數(shù)目,還有增減引用數(shù)目的函數(shù)。
這個方法是一個內(nèi)存管理技術(shù),叫做引用計數(shù),在SDS的情景下有兩個優(yōu)點:
-
降低了因沒有釋放SDS字符串或者釋放已被釋放了的字符串,而造成內(nèi)存泄露或者bug的可能性。
-
當你修改SDS字符串時,你不需要更新每一個它的引用。(因為新的SDS字符串可以指向不同的內(nèi)存位置)
盡管這顯然已經(jīng)是一項很常用的編程技術(shù)了,我還是把它的基本思路概述一下。像這樣,你創(chuàng)建一個結(jié)構(gòu)體:
struct mySharedStrings {
int refcount;
sds string;}
當新的字符串被創(chuàng)建出來,分配了這個結(jié)構(gòu)體并且返回refcount設成1。然后你有兩個函數(shù)修改共享字符串的引用數(shù)目:
-
incrementStringRefCount會簡單地將結(jié)構(gòu)體里的為1的refcount增加。每當你在新的數(shù)據(jù)結(jié)構(gòu)、變量或者隨便什么中加了一個字符串的引用,它就會被調(diào)用。
-
decrementStringRefCount被用于刪減一個引用。然而這個函數(shù)有點特殊,因為當refcount減到0時,它會自動釋放SDS字符串,以及mySharedString結(jié)構(gòu)體。
對堆檢查工具(Heap Checker)的影響
因為SDS返回一個指向由malloc分配的內(nèi)存塊的中間,堆檢查工具可能會有些問題,但是:
-
常用的Valgrind程序會發(fā)現(xiàn)SDS字符串是可能丟失的內(nèi)存,但并不是確定丟失,所以還是可以容易知道是否有泄露。我用Valgrind和Redis許多年,每一個真的泄露都會被檢測為“確定丟失”。
-
OSX工具不會把SDS字符串檢測為泄露,并能夠正確操作指向內(nèi)存塊中間的指針。
系統(tǒng)調(diào)用的零復制
此時,通過閱讀代碼,你應該已經(jīng)擁有所有挖掘更多SDS庫內(nèi)幕的工具了。然而,還有一個有趣的模式,你可以應用導出的底層API,它在Redis內(nèi)部使用過,用來改進網(wǎng)絡代碼性能。
用sdsIncrLen()和sdsMakeRoomFor(),可以應用下面的模式,來把從內(nèi)核而來的字節(jié)連接到一個sds字符串的末尾,而無需復制到一個中間的內(nèi)存緩沖區(qū)域。
oldlen = sdslen(s);s = sdsMakeRoomFor(s, BUFFER_SIZE);nread = read(fd, s+oldlen, BUFFER_SIZE);... check for nread <= 0 and handle it ...sdsIncrLen(s, nread);
sdsIncrLen記述于sds.c的代碼中。
在你的項目中嵌入SDS
這和在你的項目中拷貝sds.c和sds.h文件一樣簡單。代碼很小,每個C99編譯器應該都不帶任何問題地搞定。
開發(fā)人員和許可
SDS由Salvatore Sanfilippo開發(fā),在BDS的兩個條款許可下發(fā)布。詳情參見軟件發(fā)布中的LICENSE文件。
原文鏈接: SDS 翻譯: 伯樂在線 - cjpan
譯文鏈接: http://blog.jobbole.com/68119/
