1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        SDS動態(tài)字符串庫

        聯(lián)合創(chuàng)作 · 2023-09-28 17:36

        SDS(Simple Dynamic Strings)是一個C語言字符串庫,設計中增加了從堆上分配內(nèi)存的字符串,來擴充有限的libc字符處理的功能,使得:

        • 使用更簡便

        • 二進制安全

        • 計算更有效率

        • 而且仍舊&#8230;兼容一般的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] = &#039;A&#039;;buf[1] = &#039;B&#039;;buf[2] = &#039;C&#039;;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] = &#039;!&#039;; /* We are sure this is safe because of sdsgrowzero() */printf("%s\n&#039;, 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] = &#039;\n&#039;;s2 = sdscatrepr(s2,s1,sdslen(s1));printf("%s\n", s2);
        
        output> "a\x01\x02\n"

        這是sdscatrepr()使用的規(guī)則:

        • \和“用backslash引用。

        • 能引用特殊字符&#8217;\n&#8217;, &#8216;\r&#8217;, &#8216;\t&#8217;, &#8216;\a&#8217;以及&#8217;\b&#8217;

        • 所有其他不能通過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ù)slen指定了要單元化的字符串,另兩個字符串sepseplen是在單元化過程中用到的分割符。最后的參數(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):

        • &#8220;call&#8221;

        • &#8220;Sabrina&#8221;

        • &#8220;and&#8221;

        • &#8220;Mark Smith\n&#8221;

        基本上,不同的標記要被一個或多個空格分割,每一個標記也可以是一個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] = &#039;&#039;;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/

        瀏覽 19
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        編輯 分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        編輯 分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            欧美不卡一区二区三区 | 中国一级特黄真人毛片60岁 | 黄色视频网址在线播放 | 久久成人网豆花视频 | 亚洲香蕉在线观看 | 日日天天 | 另类TS人妖一区二区三区 | 抽插内射视频 | 欧美一区二区三区婷婷 | 九么免费版网站nba高危风险 |