Redisにおける動的文字列(SDS)の内部実装とメモリ管理

Redisでは、C言語標準の文字列(char*)を拡張した独自の動的文字列ライブラリ「SDS (Simple Dynamic Strings)」を採用しています。主なソースコードは sds.hsds.c に実装されています。

1. SDSのデータ構造

SDSは、文字列の長さに応じて複数のヘッダー構造体を使い分け、メモリ使用量を最適化しています。
typedef char *sds;

/* 構造体のパディングを無効化し、メモリを緊密に配置する */
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len;      /* 使用済みの長さ */
    uint8_t alloc;    /* 割り当て済みの全容量(ヘッダーとNULL終端を除く) */
    unsigned char flags; /* 下位3ビットで型を識別 */
    char buf[];       /* 実際のデータ(フレキシブル配列メンバ) */
};

struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len;
    uint16_t alloc;
    unsigned char flags;
    char buf[];
};

struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len;
    uint32_t alloc;
    unsigned char flags;
    char buf[];
};

struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len;
    uint64_t alloc;
    unsigned char flags;
    char buf[];
};
__attribute__ ((__packed__)) を使用することで、コンパイラによる境界調整(アライメント)のためのパディングを防いでいます。これにより、文字列のポインタ s から1バイト戻った位置(s[-1])を直接参照することで、フラグ情報を即座に取得できるようになります。

2. ヘッダーへのアクセス手法

SDSポインタは常に buf の先頭を指しています。そのため、メタデータ(長さや容量)を取得するには、ポインタ演算を用いてヘッダーの開始位置を特定します。
#define SDS_HDR_8(s) ((struct sdshdr8 *)((s)-(sizeof(struct sdshdr8))))

static inline size_t sdslen(const sds s) {
    unsigned char info_flag = s[-1];
    switch(info_flag & 7) { // 下位3ビットのタイプを確認
        case 1: // SDS_TYPE_8
            return SDS_HDR_8(s)->len;
        // 他のタイプも同様のロジックで取得
    }
    return 0;
}

3. 文字列の生成と初期化

新しいSDS文字列を生成する際、Redisは指定された初期サイズに基づいて最適なヘッダータイプを選択します。
sds create_new_sds(const void *init_data, size_t init_len) {
    void *raw_ptr;
    sds s_ptr;
    char sds_type = select_sds_type(init_len);
    
    // 空文字列の場合は、将来の拡張性を考慮してTYPE_8以上に昇格させることが多い
    if (sds_type == 0 && init_len == 0) sds_type = 1;

    int header_size = calculate_header_size(sds_type);
    raw_ptr = s_malloc(header_size + init_len + 1); // +1は'\0'のため
    
    if (raw_ptr == NULL) return NULL;
    
    s_ptr = (char*)raw_ptr + header_size;
    unsigned char *flags_ptr = (unsigned char*)s_ptr - 1;
    
    // ヘッダー情報の初期化
    setup_header(s_ptr, sds_type, init_len);
    
    if (init_len && init_data) {
        memcpy(s_ptr, init_data, init_len);
    }
    s_ptr[init_len] = '\0';
    return s_ptr;
}

4. 動的なメモリ拡張メカニズム

SDSの最大の特徴は、文字列の結合(append)時に発生するメモリ再割り当ての最適化です。
sds expand_sds_capacity(sds s, size_t needed_len) {
    size_t current_free = sds_get_free_space(s);
    if (current_free >= needed_len) return s;

    size_t current_len = sdslen(s);
    size_t target_len = current_len + needed_len;

    // 1MB未満なら2倍に拡張、1MB以上なら1MBずつ追加する(事前割り当て戦略)
    if (target_len < 1024 * 1024)
        target_len *= 2;
    else
        target_len += 1024 * 1024;

    char old_type = s[-1] & 7;
    char new_type = select_sds_type(target_len);
    
    // TYPE_5は拡張に適さないため、TYPE_8へ強制変換
    if (new_type == 0) new_type = 1;

    void *old_hdr = (char*)s - calculate_header_size(old_type);
    void *new_hdr;
    size_t new_header_size = calculate_header_size(new_type);

    if (old_type == new_type) {
        new_hdr = s_realloc(old_hdr, new_header_size + target_len + 1);
        if (new_hdr == NULL) return NULL;
        s = (char*)new_hdr + new_header_size;
    } else {
        // ヘッダーサイズが変わる場合は再配置が必要
        new_hdr = s_malloc(new_header_size + target_len + 1);
        if (new_hdr == NULL) return NULL;
        memcpy((char*)new_hdr + new_header_size, s, current_len + 1);
        s_free(old_hdr);
        s = (char*)new_hdr + new_header_size;
        s[-1] = new_type;
    }
    
    set_sds_alloc(s, target_len);
    return s;
}
この戦略により、文字列を頻繁に更新する場合でも、システムコールの回数を大幅に削減し、実行効率を高めています。

5. SDSの利点

  • 計算量 O(1) での長さ取得: strlenのように文字列を走査する必要がありません。
  • バイナリセーフ: 内部に \0 を含めることができるため、画像データやシリアライズされたバイナリも扱えます。
  • バッファオーバーフローの防止: 割り当てサイズを常に把握しているため、安全に結合操作が行えます。
  • C標準関数との互換性: 常に末尾に \0 を付与するため、printf 等の既存の関数にそのまま渡すことができます。

タグ: redis C SDS DataStructures MemoryManagement

5月17日 00:32 投稿