不同版本的 Redis 的键值对内存占用情况示例

news2024/12/23 1:38:41

不同版本的 Redis 的键值对内存占用情况示例

文章目录

  • 不同版本的 Redis 的键值对内存占用情况示例
    • Redis 6.0
      • redisObject
      • dictEntry
      • sds
        • 🍀 数据结构
        • 🍀 sdslen() 函数
        • 🍀 sdsReqType() 函数
        • 🍀 sdsHdrSize() 函数
      • 内存分配 - malloc() 函数
        • 🍀 大小类别的计算
        • 🍀 选择合适的 bin
        • 🍀 实际内存块分配
      • set [key] [value]
        • 🍀 sdsdup() 函数
        • 🍀 dictAddRaw() 函数
        • 🍀 dictSetVal() 函数
      • memory usage [key]
        • 🍀 计算 value 的字节数
        • 🍀 计算 key 的字节数
        • 🍀 计算键值对结构体 dictEntry 的字节数
        • 🍀 小结
    • Redis 7.0
      • redisObject
      • dictEntry
      • sds
        • 🍀 数据结构
        • 🍀 sdslen() 函数
        • 🍀 sdsReqType() 函数
        • 🍀 sdsHdrSize() 函数
      • 内存分配 - malloc() 函数
        • 🍀 大小类别的计算
        • 🍀 选择合适的 bin
        • 🍀 实际内存块分配
      • set [key] [value]
        • 🍀 sdsdup() 函数
        • 🍀 dictAddRaw() 函数
        • 🍀 dictSetVal() 函数
      • memory usage [key]
        • 🍀 计算 value 的字节数
        • 🍀 计算 key 的字节数
        • 🍀 计算键值对结构体 dictEntry 的字节数
        • 🍀 计算所在 db 库的字典元数据的字节数
        • 🍀 小结
    • 总结
      • 造成差异的原因
      • memory usage [key] 计算内存使用小结
      • 感悟

本文主要讨论在 Redis 6.0 与 Redis 7.0 中,以下代码设置的键值对的内存使用字节差异:

# 1(6 个 a)
set aaaaaa 12345678

# 2
memory usage aaaaaa

# 3(7 个 a)
set aaaaaaa 12345678

# 4
memory usage aaaaaaa

「1」与「3」两条命令分别设置了键值对,虽然 key 只相差 1 个字符,但在 Redis 6.0 与 Redis 7.0 中使用 memory usage [key] 命令计算出的内存使用字节数有明显差异。

  • Redis 6.0

    127.0.0.1:6379> set aaaaaa 12345678
    OK
    127.0.0.1:6379> memory usage aaaaaa
    (integer) 48
    127.0.0.1:6379> set aaaaaaa 12345678
    OK
    127.0.0.1:6379> memory usage aaaaaaa
    (integer) 49
    
  • Redis 7.0

    127.0.0.1:6379> set aaaaaa 12345678
    OK
    127.0.0.1:6379> memory usage aaaaaa
    (integer) 48
    127.0.0.1:6379> set aaaaaaa 12345678
    OK
    127.0.0.1:6379> memory usage aaaaaaa
    (integer) 56
    

Redis 6.0

环境:

  • Redis 6.0.8 源码,单机模式环境
  • Ubuntu 24.04.1 LTS,x86_64 架构(64 位操作系统)

redisObject

Redis 中的 value 对象由 redisObject 结构表示。

// 4 + 4 + 24 + 32 + 64 = 128 bits = 16 bytes
typedef struct redisObject {
    // 4 bit
    unsigned type:4;
    // 4 bit
    unsigned encoding:4;
    // #define LRU_BITS 24 即 24 bit
    unsigned lru:LRU_BITS;
    // 32 bit                      
    int refcount;
    // 64 bit(在 64 位操作系统中占 64 bit,在 32 位操作系统中占 32 bit)
    void *ptr;
} robj;

对象结构里包含的成员变量:

  • type:标识该对象的数据类型,数据类型是指 StringListHashSetZSet 等等。
  • encoding:标识该对象使用的底层数据结构,底层数据结构是指 SDSZipListSkipList 等等。
  • lru:用于内存淘汰策略的最近最少使用或最少频率使用的键值对状态信息。
  • refcount:引用计数。
  • ptr:指向底层数据结构的指针。

struct redisObject 占用字节数为 16,可使用 sizeof(robj) 计算。

dictEntry

Redis 中的键值对由 dictEntry 结构表示。

// 8 + 8 + 8 = 24 bytes
typedef struct dictEntry {
    // 8 bytes
    void *key;
    
    // 8 bytes
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    
    // 8 bytes
    struct dictEntry *next;
} dictEntry;

对象结构里包含的成员变量:

  • key:存储 key 地址的指针。
  • v:联合体,存储 value 地址或 value 本身的值。
  • next:指向链表中的下一个元素。

struct dictEntry 占用字节数为 24,可根据 sizeof(robj) 计算。

sds

🍀 数据结构
// sds 实际是字符指针的别名,指向的是 sdshdr5、sdshdr8、sdshdr16 等结构体的 buf 字符数组
typedef char *sds;
/* 注意:sdshdr5 不会作为 value 的数据结构 */
struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 低 3 bit 表示 sds 类型,高 5 bit 表示字符串有效长度(不包含结束字符 '\0') */
    char buf[]; /* 实际存储字符 */
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* 字符串有效长度,不包含结束字符 '\0' */
    uint8_t alloc; /* 为 buf 字符数组分配了的字节大小,不包含结束字符 '\0' */
    unsigned char flags; /* 低 3 bit 表示 sds 类型,高 5 bit 未使用 */
    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[];
};

根据内存分配原理,如果我们已知 buf 字符数组的起始地址,那么在此地址的基础上,将地址减去 sizeof(char),得到的地址所存储的内容就是字符变量 flags 的内容。据此,我们就可以得到对应的 sds 类型。 这一点在后面的源码分析中会有体现。

📍 __attribute__ ((__packed__)) 用于告诉编译器进行紧凑字节填充,即忽略默认的对齐规则,不进行任何字节填充。

🍀 sdslen() 函数

作用:返回字符串的有效长度,有效长度并不包含结束字符 '\0'

/* 返回字符串的有效长度 */
static inline size_t sdslen(const sds s) {
    // sds 实际是 char * 别名,因此 s[-1] 实际上将字符指针存储的地址减去 sizeof(char) 并解引用,得到字符变量 flags 存储的内容 
    unsigned char flags = s[-1];
    // 根据 flags 中存储的 sds 类型标识来判断 sds 类型,以正确得到 len 属性值,即字符串有效长度
    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;
}
🍀 sdsReqType() 函数

作用:根据字符串长度,获取至少应该使用的 sds 数据结构类型的标识。

/* 根据字符串长度 string_size,获取至少应该使用的 sds 数据结构类型的标识。 */
static inline char sdsReqType(size_t string_size) {
    // 如果字符串长度小于 2^5,则应当使用类型为 sdshr5 的结构体作为 sds 数据结构
    if (string_size < 1<<5)
        return SDS_TYPE_5;
    // 如果字符串长度小于 2^8,则应当使用类型为 sdshr8 的结构体作为 sds 数据结构
    if (string_size < 1<<8)
        return SDS_TYPE_8;
    // 如果字符串长度小于 2^8,则应当使用类型为 sdshr16 的结构体作为 sds 数据结构
    if (string_size < 1<<16)
        return SDS_TYPE_16;
    
// 条件编译,会根据操作系统架构进行动态调整代码
#if (LONG_MAX == LLONG_MAX)
    if (string_size < 1ll<<32)
        return SDS_TYPE_32;
    return SDS_TYPE_64;
#else
    return SDS_TYPE_32;
#endif
}
🍀 sdsHdrSize() 函数

作用:根据类型标识,获取对应类型的结构体(sdshdr5、sdshdr8、sdshdr16…)的占用字节大小。

/* 根据类型标识 type,获取对应类型的结构体(sdshdr5、sdshdr8、sdshdr16...)的占用字节大小。 */
static inline int sdsHdrSize(char type) {
    switch(type&SDS_TYPE_MASK) {
        case SDS_TYPE_5:
            return sizeof(struct sdshdr5);
        case SDS_TYPE_8:
            return sizeof(struct sdshdr8);
        case SDS_TYPE_16:
            return sizeof(struct sdshdr16);
        case SDS_TYPE_32:
            return sizeof(struct sdshdr32);
        case SDS_TYPE_64:
            return sizeof(struct sdshdr64);
    }
    return 0;
}

内存分配 - malloc() 函数

Redis 选择了使用 jemalloc 作为其默认的内存分配器,因此我们这里关注 jemalloc 对 malloc() 函数的实现。

/* 内存分配,size 是请求分配的内存大小,但实际分配的连续内存块大小 >= size */
void *je_malloc(size_t size) {
    void *ret;
    static_opts_t sopts;
    dynamic_opts_t dopts;

    LOG("core.malloc.entry", "size: %zu", size);

    static_opts_init(&sopts);
    dynamic_opts_init(&dopts);

    sopts.bump_empty_alloc = true;
    sopts.null_out_result_on_error = true;
    sopts.set_errno_on_error = true;
    sopts.oom_string = "<jemalloc>: Error in malloc(): out of memory\n";

    dopts.result = &ret;
    dopts.num_items = 1;

    /* 将 item_size 设置为请求分配的内存大小 size */
    dopts.item_size = size;

    /* imalloc() 函数完成内存分配,并将分配的连续内存块的起始地址存放在 dopts.result 指针指向的地址中,即 ret 中 */
    imalloc(&sopts, &dopts);

    LOG("core.malloc.exit", "result: %p", ret);

    return ret;
}
/* 返回内存分配情况的错误状态码 */
int imalloc(static_opts_t *sopts, dynamic_opts_t *dopts) {
	// ...
    
	/* We always need the tsd.  Let's grab it right away. */
	tsd_t *tsd = tsd_fetch();
	assert(tsd);
    
	if (likely(tsd_fast(tsd))) {
		/* Fast and common path. */
		tsd_assert_fast(tsd);
		sopts->slow = false;
         /* imalloc_body() 函数完成内存分配,并将分配的连续内存块的起始地址存放在 dopts->result 指针指向的地址中 */ 
		return imalloc_body(sopts, dopts, tsd);
	} else {
		sopts->slow = true;
		return imalloc_body(sopts, dopts, tsd);
	}
}
int imalloc_body(static_opts_t *sopts, dynamic_opts_t *dopts, tsd_t *tsd) {
    /* 指向实际分配的内存块的起始地址 */
    void *allocation = NULL;
    /* 用于存储请求的内存大小 */
    size_t size = 0;

    szind_t ind = 0;
    size_t usize = 0;

    /* Reentrancy is only checked on slow path. */
    int8_t reentrancy_level;

    /* 计算请求的内存大小,正常情况下,会将 *size = dopts->item_size,也就是将请求的内存大小赋值给 size 变量 */
    if (unlikely(compute_size_with_overflow(sopts->may_overflow, dopts,
                                            &size))) {
        goto label_oom; // 如果计算过程中发生溢出,则跳转到错误处理标签
    }

    // ...

    /* 核心算法开始 */

    // 如果没有特殊对齐要求,默认情况下 dopts->alignment 为 0
    if (dopts->alignment == 0) {
        /* 将请求的字节大小 size 转为索引,该索引用于定位负责处理特定大小内存块的 bin */
        ind = sz_size2index(size);
        // ...
    } else { // 如果有对齐要求
        // 根据对齐需求调整大小
        usize = sz_sa2u(size, dopts->alignment);
        // ...
    }

    // ...
    
    // imalloc_no_sample() 函数实际执行内存分配,并返回分配的连续内存块的起始地址
    allocation = imalloc_no_sample(sopts, dopts, tsd, size, usize, ind);
    if (unlikely(allocation == NULL)) {
        goto label_oom;
    }



    /* Success! */
    // 将已分配的内存块的起始地址赋给 *dopts->result
    *dopts->result = allocation;
    return 0;

    // ...
}

整个的内存分配,大致做了三件事情:

  1. 大小类别的计算
  2. 选择合适的 bin
  3. 实际内存块分配
🍀 大小类别的计算

将请求的字节大小 size 转为索引,该索引用于定位负责处理特定大小内存块的 bin。实际上,该索引不仅可以定位到 tcache_t 中对应的 cache_bin_t 实例,还可以得到请求字节大小对应的实际 jemalloc 应该分配的内存块大小,这个实际分配内存块大小等于 sz_index2size_tab[ind]

szind_t sz_size2index(size_t size) {
    assert(size > 0);
    if (likely(size <= LOOKUP_MAXCLASS)) {
        /* 将请求的字节大小 size 转为索引,该索引用于定位负责处理特定大小内存块的 bin */
        return sz_size2index_lookup(size);
    }
    return sz_size2index_compute(size);
}
#define LG_TINY_MIN		3

szind_t sz_size2index_lookup(size_t size) {
    assert(size <= LOOKUP_MAXCLASS);
    {
        /* 
         * 1.根据 size 计算 sz_size2index_tab 映射表索引:(size-1) >> LG_TINY_MIN
         * 2.从 sz_size2index_tab 映射表获取定位 bin 的索引:sz_size2index_tab[(size-1) >> LG_TINY_MIN] */
        szind_t ret = (sz_size2index_tab[(size-1) >> LG_TINY_MIN]);

        assert(ret == sz_size2index_compute(size));

        /* 返回用于定位负责处理特定大小内存块的 bin 的索引 */
        return ret;
    }
}

这里 jemalloc 实际维护了两张映射表:

  1. sz_size2index_tab:维护了从「请求字节大小」到「索引」的映射。

    /*
     * sz_size2index_tab is a compact lookup table that rounds request sizes up to
     * size classes.  In order to reduce cache footprint, the table is compressed,
     * and all accesses are via sz_size2index().
     */
    extern uint8_t const sz_size2index_tab[];
    
    数组索引(index)定位 bin/存储的 sz_index2size_tab 数组索引(value)
    00
    11
    22
    33
    44
    55
    66
    77
    88
    98
    109
    119
    1210
  2. sz_index2size_tab:维护了从「索引」到「jemalloc 应该分配的内存块大小」的映射。

    /*
     * sz_index2size_tab encodes the same information as could be computed (at
     * unacceptable cost in some code paths) by sz_index2size_compute().
     */
    extern size_t const sz_index2size_tab[NSIZES];
    
    数组索引(index)jemalloc 应该分配的内存块大小(value)
    08
    116
    224
    332
    440
    548
    656
    764
    880
    996
    10112
    11128
    12160
    13192
    14224
    2336917529027641081856
    2348070450532247928832

📑 例如:

  • 如果请求字节大小 size = 8,那么通过 sz_size2index_lookup() 计算得到的存储的 sz_index2size_tab 数组索引为 0,可得 sz_index2size_tab[0] 的值为 8。也就是说,如果请求字节大小为 8,那么 jemalloc 会为其分配 8 字节的连续内存块。
  • 如果请求字节大小 size = 9,那么通过 sz_size2index_lookup() 计算得到的存储的 sz_index2size_tab 数组索引为 1,可得 sz_index2size_tab[1] 的值为 16。也就是说,如果请求字节大小为 9,那么 jemalloc 会为其分配 16 字节的连续内存块。
🍀 选择合适的 bin

当应用程序请求分配某个大小的对象时,jemalloc 会计算出最接近且不小于该大小的类别索引,然后使用这个索引来访问 tcache_t 中对应的 cache_bin_t 进行分配。每个 cache_bin_t 实例专用于一个预定义的大小类别,从而实现了对多种不同大小内存块的支持。

也就是在源码中,有大概这样的逻辑:

szind_t ind = sz_size2index(size); // 获取大小类别的索引
cache_bin_t *bin = &tcache->bins_small[ind]; // 获取对应的 bin,以 cache_bin_t	bins_small[39] 数组为例

tcache 是什么呢?

#define NBINS			39
#define NSIZES			235

struct tcache_s {
	// ...

	/*
	 * 小对象的缓存 bin 数组
	 * 每个索引 i 位置的 bin 每次分配的内存块大小与 sz_index2size_tab 映射表中索引 i 位置存储的 jemalloc 应该分配的内存块大小相同
	 */
	cache_bin_t bins_small[NBINS];

	/*
	 * 大对象的缓存 bin 数组
	 * 每个索引 i 位置的 bin 每次分配的内存块大小与 sz_index2size_tab 映射表中索引 i+NBINS 位置存储的 jemalloc 应该分配的内存块大小相同
	 */
	cache_bin_t bins_large[NSIZES - NBINS];
};
🍀 实际内存块分配

首先需要了解 cache_bin_t 结构体:

typedef struct cache_bin_s cache_bin_t;
typedef int32_t cache_bin_sz_t;

struct cache_bin_s {
	cache_bin_sz_t low_water;

	cache_bin_sz_t ncached;

	cache_bin_stats_t tstats;

	void **avail;
};
  • avail:这是一个二级指针,存储了一个指针数组的末端边界地址。指针数组是用于存储一组指向可用内存块的指针。指针数组可看做是一个栈结构,从栈顶 -> 栈底,对应指针数组的首地址 -> 末端边界地址(低地址 -> 高地址),avail 二级指针指向的地址即栈底。
  • ncached:记录当前 bin 中有多少个可用的内存块,每次成功分配时减一,回收时加一。它也是指针数组的元素个数,即可用内存块数量。

avail[-ncached, ..., -1] 是可用内存块的指针,其中最低地址的对象将最先被分配出去。也就是说,当进行内存分配时,将栈顶元素 *(avail - ncached) 弹出,并 ncached--。源码如下:

/* 使用 bin 实例分配对应大小的内存块,返回分配的内存块的首地址 */
void *cache_bin_alloc_easy(cache_bin_t *bin, bool *success) {
	void *ret;

	/* 检查 bin 中是否有可用的缓存块 */
	if (unlikely(bin->ncached == 0)) { // 如果没有可用块
		bin->low_water = -1; // 设置低水位标记为无效值
		*success = false; // 分配失败
		return NULL; // 返回空指针表示分配失败
	}

	/* 分配成功 */
	*success = true;

	/* 
	 * 从 bin 的 avail 栈顶弹出一个内存块地址。
	 * 注意这里的减法操作是因为 avail 指向的是栈底,而 ncached 表示栈中的元素数量。
	 * 这样可以确保我们总是从栈顶获取最新的可用块。
	 */
	ret = *(bin->avail - bin->ncached);

	/* 更新缓存计数器,因为我们刚刚分配了一个块 */
	bin->ncached--;

	if (unlikely(bin->ncached < bin->low_water)) {
		bin->low_water = bin->ncached;
	}

	/* 返回分配的内存块地址 */
	return ret;
}

set [key] [value]

set [key] [value] 命令对应的处理函数为 setCommand

以在命令执行前,db 中不存在该 key 为例,setCommand() 函数会调用到核心处理函数 dbAdd()

/* 将 key-value 添加到 db 中 */
void dbAdd(redisDb *db, robj *key, robj *val) {
    // sds 是 char* 的别名,通过 sdsdup() 函数得到的实际是表示 key 的 sds 结构体 buf 字符数组首元素地址
    sds copy = sdsdup(key->ptr);
    
    int retval = dictAdd(db->dict, copy, val);

    serverAssertWithInfo(NULL,key,retval == DICT_OK);
    if (val->type == OBJ_LIST ||
        val->type == OBJ_ZSET ||
        val->type == OBJ_STREAM)
        signalKeyAsReady(db, key);
    if (server.cluster_enabled) slotToKeyAdd(key->ptr);
}

int dictAdd(dict *d, void *key, void *val)
{
    // 1.在堆中开辟 dictEntry 结构体对象空间
    // 2.将 dictEntry 存储在 db 字典中
    // 3.将 key 字符数组的首元素地址存储在 dictEntry 结构体的 void *key 指针中
    // 4.返回 dictEntry 结构体指针
    dictEntry *entry = dictAddRaw(d,key,NULL);

    if (!entry) return DICT_ERR;
    
    // 将 value 设置到 dictEntry 结构体对象 
    dictSetVal(d, entry, val);
    
    return DICT_OK;
}

我们重点关注源码中以下三个函数的作用:

  • sdsdup(key->ptr):拷贝 sds,并返回字符数组的首元素地址。
  • dictAddRaw(d, key, NULL)
    1. 在堆中开辟 dictEntry 结构体对象空间;
    2. 将 dictEntry 存储在 db 字典中;
    3. 将 key 字符数组的首元素地址存储在 dictEntry 结构体的 void *key 指针中;
    4. 返回 dictEntry 结构体指针。
  • dictSetVal(d, entry, val):将 value 设置到 dictEntry 结构体对象
🍀 sdsdup() 函数

作用:拷贝 sds,并返回字符数组的首元素地址。

sds sdsdup(const sds s) {
    /* sdslen(s) 返回字符串的有效长度 */
    return sdsnewlen(s, sdslen(s));
}
sds sdsnewlen(const void *init, size_t initlen) {
    void *sh;
    sds s;
    
    /* 根据初始化长度获取至少应该使用的 sds 数据结构类型的标识 */
    char type = sdsReqType(initlen);
    
    /* 空字符串通常为拼接而创建的,因此使用 sdshdr8 作为 sds 数据结构比 sdshdr5 更加合适 */
    if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
    
    /* 根据类型标识,获取对应类型的结构体(sdshdr5、sdshdr8、sdshdr16...)的占用字节大小 */
    /* 由于实际存储字符串的 char buf[] 是结构体最后一个成员,因此这是一个柔性数组,占用字节不会计算在使用 sizeof() 得到的结构体占用字节范围内 */
    int hdrlen = sdsHdrSize(type);
    
    /* 指向 sds 类型 —— flags 变量的指针 */
    unsigned char *fp;

    
    /* 为 sds 数据结构分配堆内存 */
    /* 请求字节大小为 hdrlen+initlen+1 = sds 结构体大小 + 字符串有效长度 + 结束字符 '\0' 的 1 个字节 */
    sh = s_malloc(hdrlen+initlen+1);
    
    // ...
    
    /* 将预期字符数组 buf 的起始地址存储到 char* s 中 */
    s = (char*)sh+hdrlen;
    /* 将 flags 变量的地址存储到 unsigned char *fp 中 */
    fp = ((unsigned char*)s)-1;
    
    /* 根据类型标识,对 sds 类型的实现数据类型结构体进行属性配置 */
    switch(type) {
        case SDS_TYPE_5: {
            *fp = type | (initlen << SDS_TYPE_BITS); /* 低 3 bit 表示 sds 类型,高 5 bit 表示字符串有效长度 */
            break;
        }
        case SDS_TYPE_8: {
            SDS_HDR_VAR(8,s); /* SDS_HDR_VAR 是一个宏函数,作用是将 sh 指针指向 sds 结构体的起始地址,以操作结构体 */
            sh->len = initlen; /* 设置字符串有效长度 */
            sh->alloc = initlen; /* 设置为 buf 字符数组分配了的字节大小 */
            *fp = type; /* 设置 sds 类型 */
            break;
        }
        case SDS_TYPE_16: {
            SDS_HDR_VAR(16,s);
            sh->len = initlen;
            sh->alloc = initlen;
            *fp = type;
            break;
        }
        case SDS_TYPE_32: {
            SDS_HDR_VAR(32,s);
            sh->len = initlen;
            sh->alloc = initlen;
            *fp = type;
            break;
        }
        case SDS_TYPE_64: {
            SDS_HDR_VAR(64,s);
            sh->len = initlen;
            sh->alloc = initlen;
            *fp = type;
            break;
        }
    }
    
    /* 字符数组拷贝 */
    if (initlen && init)
        memcpy(s, init, initlen);
    
    /* 为了兼容 c 标准字符串函数,以 '\0' 作为字符数组结束标识 */
    s[initlen] = '\0';
    
    /* 返回字符数组 buf 的起始地址 */
    return s;
}

对应的:

  • key = “aaaaaa”,字节数 initlen 为 6,由于字节数 < 32,因此使用 sdshdr5 作为 key 字符串的数据结构,则 hdrlen = sizeof(struct sdshdr5) = 1,因此调用 s_malloc() 函数时请求字节大小为 hdrlen+initlen+1 = 1+6+1 = 8,则 sz_size2index_lookup() 函数计算得到的存储的 sz_index2size_tab 数组索引为 0,而 sz_index2size_tab[0] 的值为 8,即实际分配内存块大小为 8。在配置 sdshdr5 实例属性时,设置 alloc = initlen = 6

    image-20241216185748945

  • key = “aaaaaaa”,字节数 initlen 为 7,由于字节数 < 32,因此使用 sdshdr5 作为 key 字符串的数据结构,则 hdrlen = sizeof(struct sdshdr5) = 1,因此调用 s_malloc_usable() 函数时请求字节大小为 hdrlen+initlen+1 = 1+7+1 = 9,则 sz_size2index_lookup() 函数计算得到的存储的 sz_index2size_tab 数组索引为 1,而 sz_index2size_tab[0] 的值为 16,即实际分配内存块大小为 16。在配置 sdshdr5 实例属性时,设置 alloc = initlen = 7。也就是说,Redis 不会将多分配的 7 字节作为字符数组 buf 的空间使用。

    image-20241217172017841

🍀 dictAddRaw() 函数

作用:

  1. 在堆中开辟 dictEntry 结构体对象空间;
  2. 将 dictEntry 存储在 db 字典中;
  3. 将 key 字符数组的首元素地址存储在 dictEntry 结构体的 void *key 指针中;
  4. 返回 dictEntry 结构体指针。
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{
    long index;
    dictEntry *entry;
    dictht *ht;

    if (dictIsRehashing(d)) _dictRehashStep(d);

    /* 计算 key 在 dict 哈希字典中的索引 */
    if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)
        return NULL;

    /* 由于可能的扩容,因此存在两个 dict,需要判断使用使用哪一个 */
    ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
    /* 在堆中开辟 dictEntry 结构体对象空间 */
    entry = zmalloc(sizeof(*entry));
    /* 采用头插法,并以链表形式,将 dictEntry 存储到字典索引位置 */
    entry->next = ht->table[index];
    ht->table[index] = entry;
    /* key 计数 +1 */
    ht->used++;

    /* dictSetKey 是一个宏,会替换为 entry->key = key 即将 key 字符数组的首元素地址存储在 dictEntry 结构体的 void *key 指针中 */
    dictSetKey(d, entry, key);
    /* 返回 dictEntry 结构体指针 */
    return entry;
}

对应的:

  • key = “aaaaaa”,由于 sizeof(struct dictEntry) 为 24,因此 sz_size2index_lookup() 函数计算得到的存储的 sz_index2size_tab 数组索引为 2,而 sz_index2size_tab[2] 的值为 24,即实际分配内存块大小为 24。

    image-20241216192312279

  • key = “aaaaaaa”,由于 sizeof(struct dictEntry) 为 24,因此 sz_size2index_lookup() 函数计算得到的存储的 sz_index2size_tab 数组索引为 2,而 sz_index2size_tab[2] 的值为 24,即实际分配内存块大小为 24。

    image-20241217172727903

🍀 dictSetVal() 函数

作用:将 value 设置到 dictEntry 结构体对象。这实际是一个宏函数,在预编译时期完成替换。

#define dictSetVal(d, entry, _val_) do { \
    if ((d)->type->valDup) \
        (entry)->v.val = (d)->type->valDup((d)->privdata, _val_); \
    else \
        (entry)->v.val = (_val_); \
} while(0)

这一步很简单,就是设置 dictEntry -> v.val 指针指向。但我们要重点关注的是 dictEntry -> v.val 指针或者说 robj *val 指针指向的结构体信息,因为这个结构体是 value 的实际内存存储与占用内容。

这里只看 value = “12345678” 的源码部分。由于 “12345678” 可用整型表示,为了节约内存,Redis 会使用 OBJ_ENCODING_INT 编码来进行优化。

// 返回 value 对应的 redisObject 结构体的指针
robj *createStringObjectFromLongLongWithOptions(long long value, int valueobj) {
    robj *o;

    // ...

    if (value >= LONG_MIN && value <= LONG_MAX) {
        // 创建一个 type = OBJ_STRING 的 redisObject 结构体,sizeof(struct redisObject) 为 16 字节
        o = createObject(OBJ_STRING, NULL);
        // 设置编码为 OBJ_ENCODING_INT
        o->encoding = OBJ_ENCODING_INT;
        // 复用指针变量,节省内存,把 12345678 当做地址存储。在 get 时,会根据 encoding 再从 ptr 取出值
        o->ptr = (void*)((long)value);
    }

    // ...
    
    return o;
}

对应的:

  • key = “aaaaaa”,value = “12345678”,由于 sizeof(struct redisObject) 为 16,因此 sz_size2index_lookup() 函数计算得到的存储的 sz_index2size_tab 数组索引为 1,而 sz_index2size_tab[1] 的值为 16,即实际分配内存块大小为 16。

    image-20241216195602245

  • key = “aaaaaaa”,value = “12345678”,由于 sizeof(struct redisObject) 为 16,因此 sz_size2index_lookup() 函数计算得到的存储的 sz_index2size_tab 数组索引为 1,而 sz_index2size_tab[1] 的值为 16,即实际分配内存块大小为 16。

    image-20241217173306502

memory usage [key]

memory usage [key] 命令对应的处理函数为 memoryCommand

void memoryCommand(client *c) {
    // ...

    // 1.计算 value 的字节数
    size_t usage = objectComputeSize(dictGetVal(de),samples);
    // 2.计算 key 的字节数
    usage += sdsAllocSize(dictGetKey(de));
    // 3.计算键值对结构体 dictEntry 的字节数
    usage += sizeof(dictEntry);
    
    // ...
}

usage 变量即是用于存储键值对的内存使用字节数。可以看到,共有三个部分组成:

  1. objectComputeSize(dictGetVal(de),samples):计算 value 的字节数。
  2. sdsAllocSize(dictGetKey(de)):计算 key 的字节数。
  3. sizeof(dictEntry):计算键值对结构体 dictEntry 的字节数。
🍀 计算 value 的字节数
size_t objectComputeSize(robj *o, size_t sample_size) {
    sds ele, ele2;
    dict *d;
    dictIterator *di;
    struct dictEntry *de;
    size_t asize = 0, elesize = 0, samples = 0;

    if (o->type == OBJ_STRING) {
        if(o->encoding == OBJ_ENCODING_INT) { // 执行第 1 个 if 中的语句
            asize = sizeof(*o); // sizeof(struct redisObject) = 16 bytes
        } else if(o->encoding == OBJ_ENCODING_RAW) {
            asize = sdsAllocSize(o->ptr)+sizeof(*o);
        } else if(o->encoding == OBJ_ENCODING_EMBSTR) {
            asize = sdslen(o->ptr)+2+sizeof(*o);
        } else {
            serverPanic("Unknown string encoding");
        }
    } else if (o->type == OBJ_LIST) {
        // ...
    } else if (o->type == OBJ_SET) {
        // ...
    } else if (o->type == OBJ_ZSET) {
        // ...
    } else if (o->type == OBJ_HASH) {
        // ...
    } else if (o->type == OBJ_STREAM) {
        // ...
    } else if (o->type == OBJ_MODULE) {
        // ...
    } else {
        serverPanic("Unknown object type");
    }

    return asize;
}

对应的:

  • key = “aaaaaa”,value = “12345678”,存储时 redisObject.type = OBJ_STRINGredisObject.encoding = OBJ_ENCODING_INT,则 objectComputeSize() 函数返回结果为 sizeof(struct redisObject) 即 16。
  • key = “aaaaaaa”,value = “12345678”,存储时 redisObject.type = OBJ_STRINGredisObject.encoding = OBJ_ENCODING_INT,则 objectComputeSize() 函数返回结果为 sizeof(struct redisObject) 即 16。
🍀 计算 key 的字节数
size_t sdsAllocSize(sds s) {
    // 获取 sds 结构体的 alloc 属性值,这实际是为字符数组 buf 开辟了的内存大小(不包含结束字符 '\0')
    size_t alloc = sdsalloc(s);
    // sds 结构体占用字节 + 为字符数组 buf 开辟了的内存大小 + 结束字符 '\0' 1 个字节
    // 这实际是之前 set 时对 key 进行内存分配计算出的请求内存大小,而非实际分配内存大小,redis 6.0 没有使用这多分配的空间
    return sdsHdrSize(s[-1])+alloc+1;
}

/* sdsalloc() = sdsavail() + sdslen() */
static inline size_t sdsalloc(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)->alloc;
        case SDS_TYPE_16:
            return SDS_HDR(16,s)->alloc;
        case SDS_TYPE_32:
            return SDS_HDR(32,s)->alloc;
        case SDS_TYPE_64:
            return SDS_HDR(64,s)->alloc;
    }
    return 0;
}

对应的:

  • key = “aaaaaa”,通过之前对 sdsdup() 函数的分析,请求内存大小为 sizeof(struct sdshdr5)+alloc+1=1+6+1=8
  • key = “aaaaaaa”,通过之前对 sdsdup() 函数的分析,请求内存大小为 sizeof(struct sdshdr5)+alloc+1=1+7+1=9
🍀 计算键值对结构体 dictEntry 的字节数
typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;

占用字节分析:

  • *void key:8 字节。
  • union v:联合体,8 字节。
  • *struct dictEntry next:8 字节。

综上,sizeof(struct dictEntry) 的结果为 24 字节。

🍀 小结

综上对每个函数的分析,以及 set 时的具体实现,我们得出:

类型set aaaaaa 12345678set aaaaaaa 12345678
计算 value 的字节数1616
计算 key 的字节数89
计算键值对结构体 dictEntry 的字节数2424
字节总和4849

Redis 7.0

  • Redis 7.0.14 源码,单机模式环境
  • Ubuntu 24.04.1 LTS,x86_64 架构(64 位操作系统)

redisObject

Redis 中的 value 对象由 redisObject 结构表示。

// 4 + 4 + 24 + 32 + 64 = 128 bits = 16 bytes
typedef struct redisObject {
    // 4 bit
    unsigned type:4;
    // 4 bit
    unsigned encoding:4;
    // #define LRU_BITS 24 即 24 bit
    unsigned lru:LRU_BITS;
    // 32 bit                      
    int refcount;
    // 64 bit(在 64 位操作系统中占 64 bit,在 32 位操作系统中占 32 bit)
    void *ptr;
} robj;

对象结构里包含的成员变量:

  • type:标识该对象的数据类型,数据类型是指 StringListHashSetZSet 等等。
  • encoding:标识该对象使用的底层数据结构,底层数据结构是指 SDSZipListSkipList 等等。
  • lru:用于内存淘汰策略的最近最少使用或最少频率使用的键值对状态信息。
  • refcount:引用计数。
  • ptr:指向底层数据结构的指针。

struct redisObject 占用字节数为 16,可使用 sizeof(robj) 计算。

dictEntry

Redis 中的键值对由 dictEntry 结构表示。

// 8 + 8 + 8 = 24 bytes
typedef struct dictEntry {
    // 8 bytes
    void *key;
    
    // 8 bytes
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    
    // 8 bytes
    struct dictEntry *next;
    // 空指针数组,由于是结构体最后一个成员,因此是柔性数组,不参与结构体占用字节大小计算
    void *metadata[];
} dictEntry;

对象结构里包含的成员变量:

  • key:存储 key 地址的指针。
  • v:联合体,存储 value 地址或 value 本身的值。
  • next:指向链表中的下一个元素。
  • metadata:存储与键值对相关的额外信息。

struct dictEntry 占用字节数为 24,可根据 sizeof(robj) 计算。

sds

🍀 数据结构
// sds 实际是字符指针的别名,指向的是 sdshdr5、sdshdr8、sdshdr16 等结构体的 buf 字符数组
typedef char *sds;
/* 注意:sdshdr5 不会作为 value 的数据结构 */
struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 低 3 bit 表示 sds 类型,高 5 bit 表示字符串有效长度(不包含结束字符 '\0') */
    char buf[]; /* 实际存储字符 */
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* 字符串有效长度,不包含结束字符 '\0' */
    uint8_t alloc; /* 为 buf 字符数组分配了的字节大小,不包含结束字符 '\0' */
    unsigned char flags; /* 低 3 bit 表示 sds 类型,高 5 bit 未使用 */
    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[];
};

根据内存分配原理,如果我们已知 buf 字符数组的起始地址,那么在此地址的基础上,将地址减去 sizeof(char),得到的地址所存储的内容就是字符变量 flags 的内容。据此,我们就可以得到对应的 sds 类型。 这一点在后面的源码分析中会有体现。

📍 __attribute__ ((__packed__)) 用于告诉编译器进行紧凑字节填充,即忽略默认的对齐规则,不进行任何字节填充。

🍀 sdslen() 函数

作用:返回字符串的有效长度,有效长度并不包含结束字符 '\0'

/* 返回字符串的有效长度 */
static inline size_t sdslen(const sds s) {
    // sds 实际是 char * 别名,因此 s[-1] 实际上将字符指针存储的地址减去 sizeof(char) 并解引用,得到字符变量 flags 存储的内容 
    unsigned char flags = s[-1];
    // 根据 flags 中存储的 sds 类型标识来判断 sds 类型,以正确得到 len 属性值,即字符串有效长度
    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;
}
🍀 sdsReqType() 函数

作用:根据字符串长度,获取至少应该使用的 sds 数据结构类型的标识。

/* 根据字符串长度 string_size,获取至少应该使用的 sds 数据结构类型的标识。 */
static inline char sdsReqType(size_t string_size) {
    // 如果字符串长度小于 2^5,则应当使用类型为 sdshr5 的结构体作为 sds 数据结构
    if (string_size < 1<<5)
        return SDS_TYPE_5;
    // 如果字符串长度小于 2^8,则应当使用类型为 sdshr8 的结构体作为 sds 数据结构
    if (string_size < 1<<8)
        return SDS_TYPE_8;
    // 如果字符串长度小于 2^8,则应当使用类型为 sdshr16 的结构体作为 sds 数据结构
    if (string_size < 1<<16)
        return SDS_TYPE_16;
    
// 条件编译,会根据操作系统架构进行动态调整代码
#if (LONG_MAX == LLONG_MAX)
    if (string_size < 1ll<<32)
        return SDS_TYPE_32;
    return SDS_TYPE_64;
#else
    return SDS_TYPE_32;
#endif
}
🍀 sdsHdrSize() 函数

作用:根据类型标识,获取对应类型的结构体(sdshdr5、sdshdr8、sdshdr16…)的占用字节大小。

/* 根据类型标识 type,获取对应类型的结构体(sdshdr5、sdshdr8、sdshdr16...)的占用字节大小。 */
static inline int sdsHdrSize(char type) {
    switch(type&SDS_TYPE_MASK) {
        case SDS_TYPE_5:
            return sizeof(struct sdshdr5);
        case SDS_TYPE_8:
            return sizeof(struct sdshdr8);
        case SDS_TYPE_16:
            return sizeof(struct sdshdr16);
        case SDS_TYPE_32:
            return sizeof(struct sdshdr32);
        case SDS_TYPE_64:
            return sizeof(struct sdshdr64);
    }
    return 0;
}

内存分配 - malloc() 函数

Redis 选择了使用 jemalloc 作为其默认的内存分配器,因此我们这里关注 jemalloc 对 malloc() 函数的实现。

整个的内存分配,大致做了三件事情:

  1. 大小类别的计算
  2. 选择合适的 bin
  3. 实际内存块分配
void *je_malloc(size_t size) {

    // ...

    /* 
     * 1.大小类别的计算
     * 将请求的字节大小 size 转为索引,该索引用于定位负责处理特定大小内存块的 bin */
    szind_t ind = sz_size2index_lookup(size);
    
    // ...

    /* 
     * 2.选择合适的 bin
     * 从 tcache 中获取对应大小类别的缓存 bin */
    cache_bin_t *bin = tcache_small_bin_get(tcache, ind);
    
    bool tcache_success;
    /* 
     * 3.实际内存块分配
     * 尝试从 bin 中分配内存,如果成功则设置 tcache_success 为 true,并返回分配的连续内存的起始地址 */
    void* ret = cache_bin_alloc_easy(bin, &tcache_success);

    /* 如果分配成功 */
    if (tcache_success) {
        // ...

        /* 返回分配的连续内存的起始地址 */
        return ret;
    }

    /* 如果上述过程未能成功分配内存,则使用默认的内存分配方法 */
    return malloc_default(size);
}
🍀 大小类别的计算

将请求的字节大小 size 转为索引,该索引用于定位负责处理特定大小内存块的 bin。实际上,该索引不仅可以定位到 tcache_t 中对应的 cache_bin_t 实例,还可以得到请求字节大小对应的实际 jemalloc 应该分配的内存块大小,这个实际分配内存块大小等于 sz_index2size_tab[ind]

#define SC_LG_TINY_MIN 3

szind_t sz_size2index_lookup(size_t size) {
    assert(size <= SC_LOOKUP_MAXCLASS);

    /* 
     * 1.根据 size 计算 sz_size2index_tab 映射表索引:(size + (ZU(1) << SC_LG_TINY_MIN) - 1) >> SC_LG_TINY_MIN
     * 2.从 sz_size2index_tab 映射表获取定位 bin 的索引:sz_size2index_tab[(size + (ZU(1) << SC_LG_TINY_MIN) - 1) >> SC_LG_TINY_MIN] */
    szind_t ret = (sz_size2index_tab[(size + (ZU(1) << SC_LG_TINY_MIN) - 1)
                                     >> SC_LG_TINY_MIN]);

    assert(ret == sz_size2index_compute(size));

    /* 返回存储的 sz_index2size_tab 数组索引 */
    return ret;
}

这里 jemalloc 实际维护了两张映射表:

  1. sz_size2index_tab:维护了从「请求字节大小」到「索引」的映射。

    uint8_t sz_size2index_tab[(SC_LOOKUP_MAXCLASS >> SC_LG_TINY_MIN) + 1];
    
    /* 以下初始化映射表代码不必做了解 */
    static void sz_boot_size2index_tab(const sc_data_t *sc_data) {
    	size_t dst_max = (SC_LOOKUP_MAXCLASS >> SC_LG_TINY_MIN) + 1;
    	size_t dst_ind = 0;
    	for (unsigned sc_ind = 0; sc_ind < SC_NSIZES && dst_ind < dst_max;
    	    sc_ind++) {
    		const sc_t *sc = &sc_data->sc[sc_ind];
    		size_t sz = (ZU(1) << sc->lg_base)
    		    + (ZU(sc->ndelta) << sc->lg_delta);
    		size_t max_ind = ((sz + (ZU(1) << SC_LG_TINY_MIN) - 1)
    				   >> SC_LG_TINY_MIN);
    		for (; dst_ind <= max_ind && dst_ind < dst_max; dst_ind++) {
    			sz_size2index_tab[dst_ind] = sc_ind;
    		}
    	}
    }
    
    数组索引(index)存储的 sz_index2size_tab 数组索引(value)
    00
    10
    21
    32
    43
    54
    65
    76
    87
    98
  2. sz_index2size_tab:维护了从「索引」到「jemalloc 应该分配的内存块大小」的映射。

    size_t sz_index2size_tab[SC_NSIZES];
    
    /* 以下初始化映射表代码不必做了解 */
    static void sz_boot_index2size_tab(const sc_data_t *sc_data) {
    	for (unsigned i = 0; i < SC_NSIZES; i++) {
    		const sc_t *sc = &sc_data->sc[i];
    		sz_index2size_tab[i] = (ZU(1) << sc->lg_base)
    		    + (ZU(sc->ndelta) << (sc->lg_delta));
    	}
    }
    
    数组索引(index)存储的内存块大小(value)
    08
    116
    224
    332
    440
    548
    656
    764
    880
    996
    10112
    11128
    12160
    13192
    14224
    2336917529027641081856
    2348070450532247928832

📑 例如:

  • 如果请求字节大小 size = 8,那么通过 sz_size2index_lookup() 计算得到的存储的 sz_index2size_tab 数组索引为 0,可得 sz_index2size_tab[0] 的值为 8。也就是说,如果请求字节大小为 8,那么 jemalloc 会为其分配 8 字节的连续内存块。
  • 如果请求字节大小 size = 9,那么通过 sz_size2index_lookup() 计算得到的存储的 sz_index2size_tab 数组索引为 1,可得 sz_index2size_tab[1] 的值为 16。也就是说,如果请求字节大小为 9,那么 jemalloc 会为其分配 16 字节的连续内存块。
🍀 选择合适的 bin

当应用程序请求分配某个大小的对象时,jemalloc 会计算出最接近且不小于该大小的类别索引,然后使用这个索引来访问 tcache_t 中对应的 cache_bin_t 进行分配。每个 cache_bin_t 实例专用于一个预定义的大小类别,从而实现了对多种不同大小内存块的支持。

也就是在源码中,有大概这样的逻辑:

szind_t ind = sz_size2index(size); // 获取大小类别的索引
cache_bin_t *bin = &tcache->bins_small[ind]; // 获取对应的 bin,以 cache_bin_t	bins_small[39] 数组为例

tcache 是什么呢?

typedef struct tcache_s tcache_t;

struct tcache_s {
	// ...

	/*
	 * 小对象的缓存 bin 数组
	 * 每个索引 i 位置的 bin 每次分配的内存块大小与 sz_index2size_tab 映射表中索引 i 位置存储的 jemalloc 应该分配的内存块大小相同
	 */
	cache_bin_t bins_small[SC_NBINS];

	/*
	 * 大对象的缓存 bin 数组
	 * 每个索引 i 位置的 bin 每次分配的内存块大小与 sz_index2size_tab 映射表中索引 i+SC_NBINS 位置存储的 jemalloc 应该分配的内存块大小相同
	 */
	cache_bin_t bins_large[SC_NSIZES-SC_NBINS];
};
🍀 实际内存块分配

首先需要了解 cache_bin_t 结构体:

typedef struct cache_bin_s cache_bin_t;
typedef int32_t cache_bin_sz_t;

struct cache_bin_s {
	cache_bin_sz_t low_water;

	cache_bin_sz_t ncached;

	cache_bin_stats_t tstats;

	void **avail;
};
  • avail:这是一个二级指针,存储了一个指针数组的末端边界地址。指针数组是用于存储一组指向可用内存块的指针。指针数组可看做是一个栈结构,从栈顶 -> 栈底,对应指针数组的首地址 -> 末端边界地址(低地址 -> 高地址),avail 二级指针指向的地址即栈底。
  • ncached:记录当前 bin 中有多少个可用的内存块,每次成功分配时减一,回收时加一。它也是指针数组的元素个数,即可用内存块数量。

avail[-ncached, ..., -1] 是可用内存块的指针,其中最低地址的对象将最先被分配出去。也就是说,当进行内存分配时,ncached--,并将栈顶元素 *(avail - (ncached + 1)) 弹出。源码如下:

/* 使用 bin 实例分配对应大小的内存块,返回分配的内存块的首地址 */
void *cache_bin_alloc_easy(cache_bin_t *bin, bool *success) {
	void *ret;

    /* 更新缓存计数器,因为我们准备分配一个块 */
	bin->ncached--;

	/* 检查 bin 中是否有可用的缓存块 */
	if (unlikely(bin->ncached <= bin->low_water)) {
		bin->low_water = bin->ncached;
		if (bin->ncached == -1) {
			bin->ncached = 0;
			*success = false;
			return NULL;
		}
	}

	/* 分配成功 */
	*success = true;
    
    /* 
	 * 从 bin 的 avail 栈顶弹出一个内存块地址。
	 * 注意这里的减法操作是因为 avail 指向的是栈底,而 ncached 表示栈中的元素数量。
	 * 这样可以确保我们总是从栈顶获取最新的可用块。
	 */
	ret = *(bin->avail - (bin->ncached + 1));

    /* 返回分配的内存块地址 */
	return ret;
}

set [key] [value]

set [key] [value] 命令对应的处理函数为 setCommand

以在命令执行前,db 中不存在该 key 为例,setCommand() 函数会调用到核心处理函数 dbAdd()

/* 将 key-value 添加到 db 中 */
void dbAdd(redisDb *db, robj *key, robj *val) {
    // sds 是 char* 的别名,通过 sdsdup() 函数得到的实际是表示 key 的 sds 结构体 buf 字符数组首元素地址
    sds copy = sdsdup(key->ptr);
    
    // 1.在堆中开辟 dictEntry 结构体对象空间
    // 2.将 dictEntry 存储在 db 字典中
    // 3.将 key 字符数组的首元素地址存储在 dictEntry 结构体的 void *key 指针中
    // 4.返回 dictEntry 结构体指针
    dictEntry *de = dictAddRaw(db->dict, copy, NULL);
    
    serverAssertWithInfo(NULL, key, de != NULL);
    
    // 将 value 设置到 dictEntry 结构体对象 
    dictSetVal(db->dict, de, val);
    
    signalKeyAsReady(db, key, val->type);
    if (server.cluster_enabled) slotToKeyAddEntry(de, db);
    notifyKeyspaceEvent(NOTIFY_NEW,"new",key,db->id);
}

我们重点关注源码中以下三个函数的作用:

  • sdsdup(key->ptr):拷贝 sds,并返回字符数组的首元素地址。
  • dictAddRaw(db->dict, copy, NULL)
    1. 在堆中开辟 dictEntry 结构体对象空间;
    2. 将 dictEntry 存储在 db 字典中;
    3. 将 key 字符数组的首元素地址存储在 dictEntry 结构体的 void *key 指针中;
    4. 返回 dictEntry 结构体指针。
  • dictSetVal(db->dict, de, val):将 value 设置到 dictEntry 结构体对象
🍀 sdsdup() 函数

作用:拷贝 sds,并返回字符数组的首元素地址。

sds sdsdup(const sds s) {
    /* sdslen(s) 返回字符串的有效长度 */
    return sdsnewlen(s, sdslen(s));
}
sds sdsnewlen(const void *init, size_t initlen) {
    return _sdsnewlen(init, initlen, 0);
}
sds _sdsnewlen(const void *init, size_t initlen, int trymalloc) {
    void *sh;
    sds s;
    
    /* 根据初始化长度获取至少应该使用的 sds 数据结构类型的标识 */
    char type = sdsReqType(initlen);
    
    /* 空字符串通常为拼接而创建的,因此使用 sdshdr8 作为 sds 数据结构比 sdshdr5 更加合适 */
    if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
    
    /* 根据类型标识,获取对应类型的结构体(sdshdr5、sdshdr8、sdshdr16...)的占用字节大小 */
    /* 由于实际存储字符串的 char buf[] 是结构体最后一个成员,因此这是一个柔性数组,占用字节不会计算在使用 sizeof() 得到的结构体占用字节范围内 */
    int hdrlen = sdsHdrSize(type);
    
    /* 指向 sds 类型 —— flags 变量的指针 */
    unsigned char *fp;
    size_t usable;

    /* 检查 size_t 溢出 */
    assert(initlen + hdrlen + 1 > initlen);
    
    /* 为 sds 数据结构分配堆内存,并将 jemalloc 实际分配的字节大小记录在 usable 中 */
    /* 请求字节大小为 hdrlen+initlen+1 = sds 结构体大小 + 字符串有效长度 + 结束字符 '\0' 的 1 个字节 */
    sh = trymalloc?
        s_trymalloc_usable(hdrlen+initlen+1, &usable) :
        s_malloc_usable(hdrlen+initlen+1, &usable);
    
    // ...
    
    /* 将预期字符数组的起始地址存储到 char* s 中 */
    s = (char*)sh+hdrlen;
    /* 将 flags 变量的地址存储到 unsigned char *fp 中 */
    fp = ((unsigned char*)s)-1;
    
    /* 获取柔性数组 char buf[] 可用字节大小 */
    /* usable = 总共分配的堆内存字节大小 - sizeof(sds 结构体) - 结束标识 '\0' 占 1 个字节 */ 
    usable = usable-hdrlen-1;
    if (usable > sdsTypeMaxSize(type))
        usable = sdsTypeMaxSize(type);
    
    /* 根据类型标识,对 sds 类型的实现数据类型结构体进行属性配置 */
    switch(type) {
        case SDS_TYPE_5: {
            *fp = type | (initlen << SDS_TYPE_BITS); /* 低 3 bit 表示 sds 类型,高 5 bit 表示字符串有效长度 */
            break;
        }
        case SDS_TYPE_8: {
            SDS_HDR_VAR(8,s); /* SDS_HDR_VAR 是一个宏函数,作用是将 sh 指针指向 sds 结构体的起始地址,以操作结构体 */
            sh->len = initlen; /* 设置字符串有效长度 */
            sh->alloc = usable; /* 设置为 buf 字符数组分配了的字节大小 */
            *fp = type; /* 设置 sds 类型 */
            break;
        }
        case SDS_TYPE_16: {
            SDS_HDR_VAR(16,s);
            sh->len = initlen;
            sh->alloc = usable;
            *fp = type;
            break;
        }
        case SDS_TYPE_32: {
            SDS_HDR_VAR(32,s);
            sh->len = initlen;
            sh->alloc = usable;
            *fp = type;
            break;
        }
        case SDS_TYPE_64: {
            SDS_HDR_VAR(64,s);
            sh->len = initlen;
            sh->alloc = usable;
            *fp = type;
            break;
        }
    }
    
    /* 字符数组拷贝 */
    if (initlen && init)
        memcpy(s, init, initlen);
    
    /* 为了兼容 c 标准字符串函数,以 '\0' 作为字符数组结束标识 */
    s[initlen] = '\0';
    
    /* 返回字符数组 buf 的起始地址 */
    return s;
}

对应的:

  • key = “aaaaaa”,字节数 initlen 为 6,由于字节数 < 32,因此使用 sdshdr5 作为 key 字符串的数据结构,则 hdrlen = sizeof(struct sdshdr5) = 1,因此调用 s_malloc_usable() 函数时请求字节大小为 hdrlen+initlen+1 = 1+6+1 = 8,则 sz_size2index_lookup() 函数计算得到的存储的 sz_index2size_tab 数组索引为 0,而 sz_index2size_tab[0] 的值为 8,即实际分配内存块大小 *usable = 8。在配置 sdshdr5 实例属性时,设置 alloc = usable - hdrlen - 1 = 6

    image-20241216185748945

  • key = “aaaaaaa”,字节数 initlen 为 7,由于字节数 < 32,因此使用 sdshdr5 作为 key 字符串的数据结构,则 hdrlen = sizeof(struct sdshdr5) = 1,因此调用 s_malloc_usable() 函数时请求字节大小为 hdrlen+initlen+1 = 1+7+1 = 9,则 sz_size2index_lookup() 函数计算得到的存储的 sz_index2size_tab 数组索引为 1,而 sz_index2size_tab[0] 的值为 16,即实际分配内存块大小 *usable = 16。在配置 sdshdr5 实例属性时,设置 alloc = usable - hdrlen - 1 = 14。也就是说,Redis 会将多分配的 7 字节作为字符数组 buf 的空间使用。

    image-20241216185817078

🍀 dictAddRaw() 函数

作用:

  1. 在堆中开辟 dictEntry 结构体对象空间;
  2. 将 dictEntry 存储在 db 字典中;
  3. 将 key 字符数组的首元素地址存储在 dictEntry 结构体的 void *key 指针中;
  4. 返回 dictEntry 结构体指针。
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{
    long index;
    dictEntry *entry;
    int htidx;

    if (dictIsRehashing(d)) _dictRehashStep(d);

    /* 计算 key 在 dict 哈希字典中的索引 */
    if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)
        return NULL;

    /* 由于可能的扩容,因此存在两个 dict,需要判断使用使用哪一个 */
    htidx = dictIsRehashing(d) ? 1 : 0;
    /* 字典元数据大小,在单机模式下,默认为 0 */
    size_t metasize = dictMetadataSize(d);
    /* 在堆中开辟 dictEntry 结构体对象空间 */
    entry = zmalloc(sizeof(*entry) + metasize);
    /* 如果有字典元数据,则将 (&entry)->metadata 的 metasize 个字节初始化为 0 */
    if (metasize > 0) {
        memset(dictMetadata(entry), 0, metasize);
    }
    /* 采用头插法,并以链表形式,将 dictEntry 存储到字典索引位置 */
    entry->next = d->ht_table[htidx][index];
    d->ht_table[htidx][index] = entry;
    /* key 计数 +1 */
    d->ht_used[htidx]++;

    /* dictSetKey 是一个宏,会替换为 entry->key = key 即将 key 字符数组的首元素地址存储在 dictEntry 结构体的 void *key 指针中 */
    dictSetKey(d, entry, key);
    /* 返回 dictEntry 结构体指针 */
    return entry;
}

对应的:

  • key = “aaaaaa”,由于 sizeof(struct dictEntry) 为 24,因此 sz_size2index_lookup() 函数计算得到的存储的 sz_index2size_tab 数组索引为 2,而 sz_index2size_tab[2] 的值为 24,即实际分配内存块大小为 24。

    image-20241216192312279

  • key = “aaaaaaa”,由于 sizeof(struct dictEntry) 为 24,因此 sz_size2index_lookup() 函数计算得到的存储的 sz_index2size_tab 数组索引为 2,而 sz_index2size_tab[2] 的值为 24,即实际分配内存块大小为 24。

    image-20241216192454026

🍀 dictSetVal() 函数

作用:将 value 设置到 dictEntry 结构体对象。这实际是一个宏函数,在预编译时期完成替换。

#define dictSetVal(d, entry, _val_) do { \
    if ((d)->type->valDup) \
        (entry)->v.val = (d)->type->valDup((d), _val_); \
    else \
        (entry)->v.val = (_val_); \
} while(0)

这一步很简单,就是设置 dictEntry -> v.val 指针指向。但我们要重点关注的是 dictEntry -> v.val 指针或者说 robj *val 指针指向的结构体信息,因为这个结构体是 value 的实际内存存储与占用内容。

这里只看 value = “12345678” 的源码部分。由于 “12345678” 可用整型表示,为了节约内存,Redis 会使用 OBJ_ENCODING_INT 编码来进行优化。

// 返回 value 对应的 redisObject 结构体的指针
robj *createStringObjectFromLongLongWithOptions(long long value, int valueobj) {
    robj *o;

    // ...

    if (value >= LONG_MIN && value <= LONG_MAX) {
        // 创建一个 type = OBJ_STRING 的 redisObject 结构体,sizeof(struct redisObject) 为 16 字节
        o = createObject(OBJ_STRING, NULL);
        // 设置编码为 OBJ_ENCODING_INT
        o->encoding = OBJ_ENCODING_INT;
        // 复用指针变量,节省内存,把 12345678 当做地址存储。在 get 时,会根据 encoding 再从 ptr 取出值
        o->ptr = (void*)((long)value);
    }

    // ...
    
    return o;
}

对应的:

  • key = “aaaaaa”,value = “12345678”,由于 sizeof(struct redisObject) 为 16,因此 sz_size2index_lookup() 函数计算得到的存储的 sz_index2size_tab 数组索引为 1,而 sz_index2size_tab[1] 的值为 16,即实际分配内存块大小为 16。

    image-20241216195602245

  • key = “aaaaaaa”,value = “12345678”,由于 sizeof(struct redisObject) 为 16,因此 sz_size2index_lookup() 函数计算得到的存储的 sz_index2size_tab 数组索引为 1,而 sz_index2size_tab[1] 的值为 16,即实际分配内存块大小为 16。

    image-20241216195527348

memory usage [key]

memory usage [key] 命令对应的处理函数为 memoryCommand

void memoryCommand(client *c) {
    // ...

    // 1.计算 value 的字节数
    size_t usage = objectComputeSize(c->argv[2],dictGetVal(de),samples,c->db->id);
    // 2.计算 key 的字节数
    usage += sdsZmallocSize(dictGetKey(de));
    // 3.计算键值对结构体 dictEntry 的字节数
    usage += sizeof(dictEntry);
    // 4.计算所在 db 库的字典元数据的字节数
    usage += dictMetadataSize(c->db->dict);

    // ...
}

usage 变量即是用于存储键值对的内存使用字节数。可以看到,共有四个部分组成:

  1. objectComputeSize(c->argv[2],dictGetVal(de),samples,c->db->id):计算 value 的字节数。
  2. sdsZmallocSize(dictGetKey(de)):计算 key 的字节数。
  3. sizeof(dictEntry):计算键值对结构体 dictEntry 的字节数。
  4. dictMetadataSize(c->db->dict):计算所在 db 库的字典元数据的字节数
🍀 计算 value 的字节数
size_t objectComputeSize(robj *key, robj *o, size_t sample_size, int dbid) {
    sds ele, ele2;
    dict *d;
    dictIterator *di;
    struct dictEntry *de;
    size_t asize = 0, elesize = 0, samples = 0;

    if (o->type == OBJ_STRING) {
        if(o->encoding == OBJ_ENCODING_INT) { // 执行第 1 个 if 中的语句
            // sizeof(struct redisObject) = 16 bytes
            asize = sizeof(*o);
        } else if(o->encoding == OBJ_ENCODING_RAW) {
            asize = sdsZmallocSize(o->ptr)+sizeof(*o);
        } else if(o->encoding == OBJ_ENCODING_EMBSTR) {
            asize = zmalloc_size((void *)o);
        } else {
            serverPanic("Unknown string encoding");
        }
    } else if (o->type == OBJ_LIST) {
        // ...
    } else if (o->type == OBJ_SET) {
        // ...
    } else if (o->type == OBJ_ZSET) {
        // ...
    } else if (o->type == OBJ_HASH) {
        // ...
    } else if (o->type == OBJ_STREAM) {
        // ...
    } else if (o->type == OBJ_MODULE) {
        // ...
    } else {
        serverPanic("Unknown object type");
    }
    
    return asize;
}

对应的:

  • key = “aaaaaa”,value = “12345678”,存储时 redisObject.type = OBJ_STRINGredisObject.encoding = OBJ_ENCODING_INT,则 objectComputeSize() 函数返回结果为 sizeof(struct redisObject) 即 16。
  • key = “aaaaaaa”,value = “12345678”,存储时 redisObject.type = OBJ_STRINGredisObject.encoding = OBJ_ENCODING_INT,则 objectComputeSize() 函数返回结果为 sizeof(struct redisObject) 即 16。
🍀 计算 key 的字节数
size_t sdsZmallocSize(sds s) {
    // sds s 是 sds 结构体的 char buf[] 数组首元素地址,这里根据 s 获取 sds 结构体首地址
    void *sh = sdsAllocPtr(s);
    // jemalloc 根据首地址获取分配的连续内存块字节大小
    return zmalloc_size(sh);
}

void *sdsAllocPtr(sds s) {
    // s 为 char buf[] 首元素地址
    // s[-1] 获取 type 成员地址,sdsHdrSize(s[-1]) 则是根据 type 获取 sds 结构体占用字节
    // 两者相减,就可以得到 sds 结构体首元素地址了
    return (void*) (s-sdsHdrSize(s[-1]));
}

对应的:

  • key = “aaaaaa”,通过之前对 sdsdup() 函数的分析,可得 jemalloc 实际为 key 分配了 8 字节的连续内存。
  • key = “aaaaaaa”,通过之前对 sdsdup() 函数的分析,可得 jemalloc 实际为 key 分配了 16 字节的连续内存。
🍀 计算键值对结构体 dictEntry 的字节数
typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
    void *metadata[];
} dictEntry;

占用字节分析:

  • *void key:8 字节。
  • union v:联合体,8 字节。
  • *struct dictEntry next:8 字节。
  • void *metadata[]:柔性数组,不参与 sizeof(struct dictEntry) 计算。

综上,sizeof(struct dictEntry) 的结果为 24 字节。

🍀 计算所在 db 库的字典元数据的字节数
/*
 * 返回 db 字典条目元数据的大小(以字节为单位)。
 * 在集群模式下,元数据用于构造属于同一集群槽的 dict 条目的双向链表。 */
size_t dictEntryMetadataSize(dict *d) {
    UNUSED(d);
    return server.cluster_enabled ? sizeof(clusterDictEntryMetadata) : 0;
}

在单机环境下,默认该函数的返回值为 0。

🍀 小结

综上对每个函数的分析,以及 set 时的具体实现,我们得出:

类型set aaaaaa 12345678set aaaaaaa 12345678
计算 value 的字节数1616
计算 key 的字节数816
计算键值对结构体 dictEntry 的字节数2424
计算所在 db 库的字典元数据的字节数00
字节总和4856

总结

造成差异的原因

通过上面对源码的分析,其实我们就可以知道 memory usage [key] 分析得到的内存使用情况为什么会有差异了。

首先需要说明的是,Redis 6.0 与 Redis 7.0 都为 key = "aaaaaaa" 都请求了 9 字节的内存字节大小,但 jemalloc 实际都分配了 16 字节的连续内存块,但是对于多出来的 7 字节却持有不同的态度。

  • Redis 6.0 中,不会将多分配的 7 字节作为 sds 结构体中的字符数组 buf 的空间使用,即会设置成员 alloc = initlen = 7
  • Redis 7.0 中,将多分配的 7 字节作为 sds 结构体中的字符数组 buf 的空间使用,即会设置成员 alloc = usable - hdrlen - 1 = 14

对应的在使用 memory usage [key] 计算内存占用时:

  • Redis 6.0 中,key 的字节数 = sdsHdrSize(s[-1]) + alloc + 1 = sds 结构体占用字节 + 为字符数组 buf 开辟了的内存大小 + 结束字符 ‘\0’ 1 个字节,即 9 个字节。
  • Redis 7.0 中,key 的字节数 = jemalloc 为 key 实际分配的连续内存块大小,即 16 个字节。

从这里我们可以看出,Redis 7.0 相较于 Redis 6.0,对于 jemalloc 实际分配的额外内存空间,进行了优化利用。

memory usage [key] 计算内存使用小结

Redis 6.0:

类型set aaaaaa 12345678set aaaaaaa 12345678
计算 value 的字节数1616
计算 key 的字节数89
计算键值对结构体 dictEntry 的字节数2424
字节总和4849

Redis 7.0:

类型set aaaaaa 12345678set aaaaaaa 12345678
计算 value 的字节数1616
计算 key 的字节数816
计算键值对结构体 dictEntry 的字节数2424
计算所在 db 库的字典元数据的字节数00
字节总和4856

感悟

最后,通过本文对源码的分析,我们可以认识到:

  1. Redis 使用 jemalloc 作为默认的内存分配器,这使得它能够更有效地管理内存分配。jemalloc 会根据请求的大小选择最合适的内存块,从而减少内部碎片并提高分配效率。
  2. 对于简单的数值型字符串,如果它们可以被表示为长整数(long),Redis 会选择使用 OBJ_ENCODING_INT 编码来节省空间。这种方式不仅减少了内存占用,而且加快了数据访问速度。
  3. 在设计数据结构时,考虑到字节对齐规则,以确保最佳性能,在本文分析中,在计算字节时并没有提到结构体字节对齐,这是因为 Redis 对数据结构的巧妙设计使得无需进行字节填充。此外,柔性数组用于 sds 结构体中,允许动态增长字符缓冲区而不增加额外的指针开销。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2263984.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

捕虫游戏-项目制作

前言 同学们前面已经学习了html css javascript基础部分了&#xff0c;为了巩固和熟练前面所学的知识&#xff0c;从今天起&#xff0c;我们要做一个捕虫游戏的项目。通过项目实战夯实基础&#xff0c;将所学的知识真正用到实战中&#xff0c;强化对网页设计的能力&#xff…

用docker快速安装电子白板Excalidraw绘制流程图

注&#xff1a;本文操作以debian12.8 最小化安装环境为host系统。 一、彻底卸载原有的残留 apt-get purge docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin docker-ce-rootless-extras 二、设置docker的安装源 # Add Dockers official G…

【从零开始入门unity游戏开发之——C#篇23】C#面向对象继承——`as`类型转化和`is`类型检查、向上转型和向下转型、里氏替换原则(LSP)

文章目录 一、as类型转化和is类型检查1、as 关键字使用场景&#xff1a;语法&#xff1a;示例&#xff1a;特点&#xff1a; 2、is 关键字使用场景&#xff1a;语法&#xff1a;示例&#xff1a;特点&#xff1a; 3、总结 二、向上转型和向下转型1、向上转型示例&#xff1a; 2…

Android GO 版本锁屏声音无效问题

问题描述 Android go版本 在设置中打开锁屏音开关&#xff0c;息屏灭屏还是无声音 排查 vendor\mediatek\proprietary\packages\apps\SystemUI\src\com\android\systemui\keyguard\KeyguardViewMediator.java private void setupLocked() {...String soundPath Settings.G…

跟着问题学18——transformer模型详解及代码实战(3)Encode编码器

跟着问题学18——transformer模型详解及代码实战&#xff08;1&#xff09; 跟着问题学18——transformer详解(2)多头自注意力机制-CSDN博客 2.3 残差连接 通过自注意力层我们挖掘提取了原始数据的特征&#xff0c;但编码层中会有多个编码器&#xff0c;这会导致网络层数的加…

React系列(八)——React进阶知识点拓展

前言 在之前的学习中&#xff0c;我们已经知道了React组件的定义和使用&#xff0c;路由配置&#xff0c;组件通信等其他方法的React知识点&#xff0c;那么本篇文章将针对React的一些进阶知识点以及React16.8之后的一些新特性进行讲解。希望对各位有所帮助。 一、setState &am…

【原生js案例】移动端如何实现页面的入场和出场动画

好的css动画&#xff0c;能给用户体验带来很大的提升&#xff0c;同时也能增加app的趣味性&#xff0c;给人眼前一亮的感觉。那如何实现这种全屏的弹窗入场和退场的动画 实现效果 代码实现 UI样式美化 #musicDetails{width: 100%;height: 100%;top:0;left:0;position: absol…

Pyqt6在lineEdit中输入文件名称并创建或删除JSON文件

1、创建JSON文件 代码 import osdef addModulekeyWordFile(self):if "" ! self.lineEdit_module.text():moduleFile self.lineEdit_module.text() .jsonelse:self.toolLogPrinting(请输入模块名称)returnfilePath modulekeyWordFileDir moduleFileif os.path.e…

鸿蒙UI开发——组件滤镜效果

1、概 述 ArkUI为组件提供了滤镜效果设置&#xff0c;背景滤镜、前景滤镜、合成滤镜。我们可以通过以下方式为组件设置对应的滤镜效果。 Text(前景滤镜)// ..其他属性.foregroundFilter(filterTest1) // 通过 foregroundFilter 设置模糊效果Text(背景滤镜)// ...其他属性.bac…

均方误差损失函数(MSE)和交叉熵损失函数详解

为什么需要损失函数 前面的文章我们已经从模型角度介绍了损失函数&#xff0c;对于神经网络的训练&#xff0c;首先根据特征输入和初始的参数&#xff0c;前向传播计算出预测结果&#xff0c;然后与真实结果进行比较&#xff0c;得到它们之间的差值。 损失函数又可称为代价函…

抓包 127.0.0.1 (loopback) 使用 tcpdump+wireshark

直接使用 wireshark无法抓取 127.0.0.1环回的数据包&#xff0c;一种解决方法是先传到路由器再返回&#xff0c;但这样可能造成拥塞。 Linux 先使用tcpdump抓包并输出为二进制文件&#xff0c;然后wireshark打开。 比如 sudo tcpdump -i lo src host localhost and dst host…

免费GIS工具箱:轻松将glb文件转换成3DTiles文件

在GIS地理信息系统领域&#xff0c;GLB文件作为GLTF文件的二进制版本&#xff0c;主要用于3D模型数据的存储和展示。然而&#xff0c;GLB文件的使用频率相对较低&#xff0c;这是因为GIS系统主要处理的是地理空间数据&#xff0c;如地图、地形、地貌、植被、水系等&#xff0c;…

安防监控Liveweb视频汇聚融合平台助力执法记录仪高效使用

Liveweb平台可接入的设备除了常见的智能分析网关与摄像头以外 &#xff0c;还可通过GB28181协议接入执法记录仪&#xff0c;实现对执法过程的全程监控与录像&#xff0c;并对执法轨迹与路径进行调阅回看。那么&#xff0c;如何做到执法记录仪高效使用呢&#xff1f; 由于执法记…

【Unity3D】实现可视化链式结构数据(节点数据)

关键词&#xff1a;UnityEditor、可视化节点编辑、Unity编辑器自定义窗口工具 使用Newtonsoft.Json、UnityEditor相关接口实现 主要代码&#xff1a; Handles.DrawBezier(起点&#xff0c;终点&#xff0c;起点切线向量&#xff0c;终点切线向量&#xff0c;颜色&#xff0c;n…

网络安全核心目标CIA

网络安全的核心目标是为关键资产提供机密性(Confidentiality)、可用性(Availablity)、完整性(Integrity)。作为安全基础架构中的主要的安全目标和宗旨&#xff0c;机密性、可用性、完整性频频出现&#xff0c;被简称为CIA&#xff0c;也被成为你AIC&#xff0c;只是顺序不同而已…

[项目代码] YOLOv8 遥感航拍飞机和船舶识别 [目标检测]

项目代码下载链接 &#xff1c;项目代码&#xff1e;YOLO 遥感航拍飞机和船舶识别&#xff1c;目标检测&#xff1e;https://download.csdn.net/download/qq_53332949/90163939YOLOv8是一种单阶段&#xff08;one-stage&#xff09;检测算法&#xff0c;它将目标检测问题转化为…

去雾Cycle-GAN损失函数

文章目录 GAN-LossIdentity-LossDP-lossCycle-Loss G和F都是生成器 G是hazy → \to → gt F是gt → \to → hazy D y D_y Dy​判别无雾图是真实还是生成的&#xff1f; D x D_x Dx​判别有雾图是真实还是生成的&#xff1f; GAN-Loss 在 DAM-CCGAN 中存在两个判别器 D x D_x D…

2024年企业中生成式 AI 的现状报告

从试点到生产&#xff0c;企业 AI 格局正在被实时改写。我们对 600 名美国企业 IT 决策者进行了调查&#xff0c;以揭示新兴的赢家和输家。 从试点到生产 2024 年标志着生成性人工智能成为企业关键任务的一年。这些数字讲述了一个戏剧性的故事&#xff1a;今年人工智能支出飙升…

组件十大传值

一、defineProps 和 defineEmits defineProps 用于定义子组件接收的 props&#xff0c;即父组件传递给子组件的数据。 接收父组件传递的数据&#xff1a;定义子组件可以接受的属性及其类型。类型检查&#xff1a;确保传递的数据符合预期的类型。 defineEmits 用于定义子组件…

WPF 依赖属性和附加属性

除了普通的 CLR 属性&#xff0c; WPF 还有一套自己的属性系统。这个系统中的属性称为依赖属性。 1. 依赖属性 为啥叫依赖属性&#xff1f;不叫阿猫阿狗属性&#xff1f; 通常我们定义一个普通 CLR 属性&#xff0c;其实就是获取和设置一个私有字段的值。假设声明了 100 个 …