ziplist分析

這裏直接貼ziplist.c中的作者的註釋,從註釋中我們可以直觀的看到ziplist是用於存儲string字符串的雙向鏈表,其目的是更好地利用內存去存儲數據,減少無用數據的比例。

/* The ziplist is a specially encoded dually linked list that is designed
 * to be very memory efficient. It stores both strings and integer values,
 * where integers are encoded as actual integers instead of a series of
 * characters. It allows push and pop operations on either side of the list
 * in O(1) time. However, because every operation requires a reallocation of
 * the memory used by the ziplist, the actual complexity is related to the
 * amount of memory used by the ziplist.
 * 
 * ziplist是一個特殊編碼的的雙鏈表,他被設計成能夠高效利用內存。它能夠存儲字符串和整數,其中
 * 整數被編碼爲真正的數字而不是一連串的字符。他允許在O(1)時間內對齊從兩邊進行push、pop操作。
 * 然而,因爲每個操作需要內存的重新分配,這個真實的複雜度依賴於ziplist總計使用的內存數。
 * ----------------------------------------------------------------------------
 *
 * ZIPLIST OVERALL LAYOUT:
 * The general layout of the ziplist is as follows:
 * <zlbytes><zltail><zllen><entry><entry><zlend>
 * 
 * <zlbytes> is an unsigned integer to hold the number of bytes that the
 * ziplist occupies. This value needs to be stored to be able to resize the
 * entire structure without the need to traverse it first.
 * <zlbytes> 是一個存儲ziplist佔用字節數量的無符號整數。這個值需要存儲,
 * 從而當想要resize整個結構時,我們不必先遍歷他
 * 
 * <zltail> is the offset to the last entry in the list. This allows a pop
 * operation on the far side of the list without the need for full traversal.
 * <zltail> 是距離鏈表最後一個條目的偏移量。這樣可以不用整個遍歷而對遠端進行一次pop操作。
 * 
 * <zllen> is the number of entries.When this value is larger than 2**16-2,
 * we need to traverse the entire list to know how many items it holds.
 * <zllen>是條目數量。當這個值超過2^16 - 2,我們需要全部遍歷整個列表才能知道他有多少項
 * 
 * <zlend> is a single byte special value, equal to 255, which indicates the
 * end of the list.
 * <zelend>是一個等於255的特殊字節,它指示鏈表的結束
 * 
 * ZIPLIST ENTRIES:
 * ZIP列表條目
 * Every entry in the ziplist is prefixed by a header that contains two pieces
 * of information. First, the length of the previous entry is stored to be
 * able to traverse the list from back to front. Second, the encoding with an
 * optional string length of the entry itself is stored.
 * 每個ziplist中的條目都有一個頭部前綴,這個前綴包含兩部分信息。首先,存儲上一個
 * 條目的長度使其能夠從後向前遍歷。第二,編碼和一個可選的自身存儲的條目的字符串長度。
 * 
 * The length of the previous entry is encoded in the following way:
 * If this length is smaller than 254 bytes, it will only consume a single
 * byte that takes the length as value. When the length is greater than or
 * equal to 254, it will consume 5 bytes. The first byte is set to 254 to
 * indicate a larger value is following. The remaining 4 bytes take the
 * length of the previous entry as value.
 * (第一部分)
 * 前一個條目的長度使用以下方式進行編碼:
 * 如果這個長度小於254字節,他會僅佔用一個字節,將長度設置爲其值
 * 當這個長度超過或者等於254時,他會佔用5個字節。
 * 第一個字節會被設置爲254去指明其後跟隨了一個更大的數字。剩餘的4字節將上一個字節的長度作爲其值。
 * 
 * The other header field of the entry itself depends on the contents of the
 * entry. When the entry is a string, the first 2 bits of this header will hold
 * the type of encoding used to store the length of the string, followed by the
 * actual length of the string. When the entry is an integer the first 2 bits
 * are both set to 1. The following 2 bits are used to specify what kind of
 * integer will be stored after this header. An overview of the different
 * types and encodings is as follows:
 * 
 * (第二部分)
 * 其餘的頭部領域依賴於條目內容。當這個條目是一個string時,頭部的前兩個bits
 * 會保存存儲字符串長度的編碼類型,其後跟隨的爲字符串的實際長度。
 * 當這個條目是一個整數時,前兩個bit均會設置爲1.接下來的兩個bit會被用於指明什麼類型的整數
 * 會被存儲。
 * 一個不同的類型和編碼概述如下:
 * |00pppppp| - 1 byte
 *      String value with length less than or equal to 63 bytes (6 bits).
 *      只使用6bit去指明字符串的長度
 * |01pppppp|qqqqqqqq| - 2 bytes
 *      String value with length less than or equal to 16383 bytes (14 bits).
 *      使用14bits去存儲字符串長度,最高到達16383長度
 * |10______|qqqqqqqq|rrrrrrrr|ssssssss|tttttttt| - 5 bytes
 *      String value with length greater than or equal to 16384 bytes.
 *      使用4bytes去存儲這個字符串,2^32能夠表達4G的容量,所以只要大於16384bytes的
 *      字符串都將使用這種方式進行操作
 * 
 * 接下來的的條目表示整數
 * |11000000| - 1 byte
 *      Integer encoded as int16_t (2 bytes).
 *      00 指明整數編碼爲int16_t
 * |11010000| - 1 byte
 *      Integer encoded as int32_t (4 bytes).
 *      01
 * |11100000| - 1 byte
 *      Integer encoded as int64_t (8 bytes).
 *      10 整數編碼爲64_t
 * |11110000| - 1 byte
 *      Integer encoded as 24 bit signed (3 bytes).
 *      11 整數編碼爲24byte的符號整數
 * |11111110| - 1 byte  // 1 字節編碼數字這麼奇怪的原因在後面
 *      Integer encoded as 8 bit signed (1 byte).
 *      111110  8bit符號整數
 * |1111xxxx| - (with xxxx between 0000 and 1101) immediate 4 bit integer.
 *      Unsigned integer from 0 to 12. The encoded value is actually from
 *      1 to 13 because 0000 and 1111 can not be used, so 1 should be
 *      subtracted from the encoded 4 bit value to obtain the right value.
 *      1111xxxx 編碼的,我們使用最後的xxxx表示4bit整數,由於我們沒辦法使用1111,1111、
 *      1111,1110 和 1111,0000這三個數字,所以我們只能表示1到13。我們想要0-12就要將最後結果值減1;
 * |11111111| - End of ziplist.
 */

這裏我分析幾個關鍵問題,這些問題在源代碼中作者也有介紹。

其高效利用內存是如何實現的

其實高效的意思就是我們要存儲更多的有效數據,避免對無效數據的存儲。例如傳統的鏈表,如果我們僅僅存儲一個int,結構體的定義一般爲

struct node{
 	int val;
 	struct node *next;
 }

這個結構體中有效數據僅爲50%,因爲另一半數據我們必須拿去維護鏈表結構。所以對於存儲小型數據來說,鏈表結構就相對性價比較低。
除此之外還有一種浪費就是,假設我們存儲數據{123, 12, 41, 31},我們使用int(4 bytes)去存儲,但是實際上我們可以僅使用char(1 bytes)就可以完成這些數據的存儲。這將節省下將近四倍的空間。如果存儲的數據中大多是這種small int,那麼選取合適的int是完全能夠達到降低內存使用的目標的。

ziplist要求佔用一個連續的內存空間,每個數據存儲在每個entry中。爲了滿足entry不定長的需求,我們需要在當前節點中記錄自身節點的大小和前一個節點的大小(實現雙向鏈表)。那麼這兩個int我們也需要選擇合適的大小去存儲,否則我們還是會陷入爲了維護結構而佔用過多存儲比例的結構。(數組就是一個存儲率100%的數據結構,應爲他的結構是通過其內存位置關係來維護,沒有代價)。

通過時間換取空間,其操作更爲複雜

由於我們對於每個entry都進行了編碼操作,例如我們查看prelen,我們需要先去判斷他是採用了5 bytes存儲還是1bytes存儲,如果採用了1bytes存儲,那麼其值爲p[0]否則爲p[1:4]
並且我們如果想要獲取len,在未獲取prelen的具體存儲情況下根本無法獲得,因爲我們無法得知len的起始位置。
由於ziplist要求必須存儲在一個連續的內存空間當中,所以當我們對其進行插入或者刪除時,我們必須對其進行zreallocmemcpy。並且其操作還需要像鏈表那樣只能挨個尋找,無法實現隨即查詢。

結構的波動問題

我們假設存儲的每個entry爲253 bytes。這樣的entrys一共有100多個。那麼我們來看一個問題:
如果此時我們在頭部插入一個500 bytes的entry。當元素插入成功後,我們需要修改next entryprelen,此時我們可以看到原先的1 bytes prelen無法存儲下500,那麼我們需要將其誇大至5 bytes(既然面對擴容,我們就必須調用zreallocmemcpy)。然而這還沒完,當我們對next entry進行擴容完畢時,next next entryprelen對應值將不再是253而應是257,我們還需要對next next entry進行擴容以讓其prelen能夠容納下257,以此類推我們將會執行100次zreallocmemcpy,而這一切都是爲了完成一次插入。
作者在源代碼中是這樣描述這個問題的

/* When an entry is inserted, we need to set the prevlen field of the next
 * entry to equal the length of the inserted entry. It can occur that this
 * length cannot be encoded in 1 byte and the next entry needs to be grow
 * a bit larger to hold the 5-byte encoded prevlen. This can be done for free,
 * because this only happens when an entry is already being inserted (which
 * causes a realloc and memmove). However, encoding the prevlen may require
 * that this entry is grown as well. This effect may cascade throughout
 * the ziplist when there are consecutive entries with a size close to
 * ZIP_BIGLEN, so we need to check that the prevlen can be encoded in every
 * consecutive entry.
 *
 * Note that this effect can also happen in reverse, where the bytes required
 * to encode the prevlen field can shrink. This effect is deliberately ignored,
 * because it can cause a "flapping" effect where a chain prevlen fields is
 * first grown and then shrunk again after consecutive inserts. Rather, the
 * field is allowed to stay larger than necessary, because a large prevlen
 * field implies the ziplist is holding large entries anyway.
 *
 * The pointer "p" points to the first entry that does NOT need to be
 * updated, i.e. consecutive fields MAY need an update. */

/* 當一個entry被插入時,我們需要將next entry的prevlen設置爲插入的entry。
 * 這可能會導致當前entry的長度無法被編碼爲1bytes長度,而next entry需要增長從而存儲5 bytes編碼的prelen
 * 這個可以不用擔心,因爲這個僅發生在entry已經被插入時(插入時將會導致realloc 和 memmove)。
 * 然而,編碼這個prevlen可能需要此entry同樣增長。
 * 當有一連串entries其大小都相近於ZIP_BIGLEN,這個影響可能造成壞的影響對於整個ziplist。
 * 所以我們需要去檢查每個prelen是否都能編碼進entry
 * 
 * 注意需要編碼prevlen的bytes可以收縮,這個影響會造成相反的效果。
 * 這個影響我們故意忽略,因爲他會造成一種波動的影響。當一條鏈發生增長之後可能會由於新的插入導致收縮。
 * 我們寧願這個區域的大小超過實際需求,因爲大的prelen暗指ziplist中存儲着大的entries。
*/
static unsigned char *__ziplistCascadeUpdate(unsigned char *zl, unsigned char *p) {
    size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl)), rawlen, rawlensize;
    size_t offset, noffset, extra;
    unsigned char *np;
    zlentry cur, next;

    while (p[0] != ZIP_END) {
        // cur 爲 當前entry的結構體 
        cur = zipEntry(p);
        rawlen = cur.headersize + cur.len;
        rawlensize = zipPrevEncodeLength(NULL,rawlen);

        /* Abort if there is no next entry. */
        if (p[rawlen] == ZIP_END) break;

        // next 爲下一個entry的結構體
        next = zipEntry(p+rawlen);

        /* Abort when "prevlen" has not changed. */
        // 當prevlen未發生改變時,我們跳出循環
        if (next.prevrawlen == rawlen) break;

        // 當prevlen發生改變時,我們需要修改他
        if (next.prevrawlensize < rawlensize) {
            /* The "prevlen" field of "next" needs more bytes to hold
             * the raw length of "cur". */
            /* next的 prevlen 需要更多的bytes 去存儲 cur 的長度*/
            offset = p-zl;
            extra = rawlensize-next.prevrawlensize;
            zl = ziplistResize(zl,curlen+extra);
            p = zl+offset;

            /* Current pointer and offset for next element. */
            np = p+rawlen;
            noffset = np-zl;

            /* Update tail offset when next element is not the tail element. */
            if ((zl+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))) != np) {
                ZIPLIST_TAIL_OFFSET(zl) =
                    intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+extra);
            }

            /* Move the tail to the back. */
            memmove(np+rawlensize,
                np+next.prevrawlensize,
                curlen-noffset-next.prevrawlensize-1);
            zipPrevEncodeLength(np,rawlen);

            /* Advance the cursor */
            /* 進一步遞歸循環,保證整個ziplist正確性 */
            p += rawlen;
            curlen += extra;
        } else {
            if (next.prevrawlensize > rawlensize) {
                /* This would result in shrinking, which we want to avoid.
                 * So, set "rawlen" in the available bytes. */
                // 這個將導致收縮,但是我們想要避免這樣。所以我們直接設置rawlen 進入相應bytes中
                zipPrevEncodeLengthForceLarge(p+rawlen,rawlen);
            } else {
                // 剛好容納地下,我們此時只用改一下大小就可以
                zipPrevEncodeLength(p+rawlen,rawlen);
            }

            /* Stop here, as the raw length of "next" has not changed. */
            break;
        }
    }
    return zl;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章