字典:dict.c/dict.h

Redis 源码分析(1):字典和哈希表(dict.c 和 dict.h)
http://huangz.iteye.com/blog/1455808
两个点:
字典结构的运作流程
哈希表的渐进式 rehash操作
哈希表是 redis 的核心结构之一,在 redis 的源码中, dict.c 和 dict.h 就定义了哈希结构。

dict 、 dictht 和 dictEntry 这三个核心数据结构

/* 字典结构 */
typedef struct dict {
dictType *type; // 为哈希表中不同类型的值所使用的一族函数
void *privdata; //传给类型特定函数的可选参数
dictht ht[2]; // 每个字典使用两个哈希表
int rehashidx; // 指示 rehash 是否正在进行,如果不是则为 -1
int iterators; // 当前正在使用的 iterator 的数量
} dict;

代码的注释基本都说明相关属性的作用了,需要补充的一些是:

每个字典使用两个哈希表,是因为要实现渐增式 rehash ,redis 会逐个逐个地将 0 号哈希表的元素移动到 1 号哈希表,直到 0 号哈希表被清空为止,

另外, rehashidx 记录的实际上是 rehash 进行到的索引,比如如果 rehash 进行到第 10 个元素,那么 rehashidx 的值就为 9,以此类推,如果没有在进行 rehash ,rehashidx 的值就为 -1 。

哈希表结构 —— dictht 结构,这个哈希表是一个 separate chaining hash table 实现,它通过将哈希值相同的元素放到一个链表中来解决冲突问题:
typedef struct dictht {
dictEntry **table; // 节点指针数组
unsigned long size; // 桶的数量
unsigned long sizemask; // mask 码,用于地址索引计算
unsigned long used; // 已有节点数量
} dictht;

table 属性组成了一个数组,数组里带有节点指针,用作链表。

size 、 sizemask 和 used 这三个属性初看上去让人有点头晕,实际上,它们分别代表的是:
size :桶的数量,也即是, table 数组的大小。
sizemask :这个值通过 size - 1 计算出来,给定 key 的哈希值计算出来之后,就会和这个数值进行 & 操作,决定元素被放到 table 数组的那一个位置上。
used :这个值代表目前哈希表中元素的数量,也即是哈希表总共保存了多少 dictEntry 结构。

链表节点结构
typedef struct dictEntry {
void *key; // 键
union {
void *val;
uint64_t u64;
int64_t s64;
} v; // 值(可以有几种不同类型)
struct dictEntry *next; // 指向下一个哈希节点(形成链表)
} dictEntry;

字典创建流程

在初步解了几个核心数据结构之后,是时候可以来看看相关的函数怎么来使用这些数据结构了,让我们从最开始的创建字典开始,一步步研究字典(以及哈希表)的运作流程。

因为调用流程可以给我们一个高层次的观点来了解数据结构的运作流程,而不必陷入到代码细节中,因此,文章这里只给出程序调用流程的部分核心代码,如果你对代码的其他细节有兴趣,可以到我的 github 上去找注释版的代码,上面有完整的代码,而且我给大部分函数都加上了注释。

OK,说回来字典这边,创建新字典执行的调用链是: dictCreate -> _dictInit -> _dictReset

其中 dictCreate 函数为 dict 结构分配了空间,然后将新的 dict 传给 _dictInit 函数,让它初始化 dict 结构的相关属性,而 _dictInit 又调用 _dictReset ,对字典的 ht 属性(也即是两个哈希表)进行常量属性的设置。

注意, _dictReset 只是为字典所属的两个哈希表进行常量属性的设置(size、 sizemask 和 used),但并不为哈希表的链表数组分配内存:

static void _dictReset(dictht *ht)
{
ht->table = NULL;
ht->size = 0;
ht->sizemask = 0;
ht->used = 0;
}

0 号哈希表的创建流程

我们知道,一个 dict 结构使用两个哈希表,也即是 d->ht[0] 和 d->ht[1] ,为了称呼方便,我们将他们分别叫做 0 号和 1 号哈希表。

从上一节可以知道, dictCreate 并不为哈希表的链表数组分配内存( d->ht[0]->table 和 d->ht[1]->table 都被设为 NULL),那么,什么时候哈希表的链表数组会被初始化呢?

答案是,当首次通过 dictAdd 向字典添加元素的时候, 0 号哈希表的链表数组会被初始化。

首次向字典增加元素将执行以下的调用序列: dictAdd -> dictAddRaw -> _dictKeyIndex -> dictExpandIfNeeded -> dictExpand

其中 dictAdd 是 dictAddRaw 的调用者, dictAddRaw 是添加元素这一工作的底层实现,而 dictAddRaw 为了计算新元素的 key 的地址索引,会调用 _dictKeyIndex :

dictEntry *dictAddRaw(dict *d, void *key)
{
// 被省略的代码...

// 计算 key 的 index 值
// 如果 key 已经存在,_dictKeyIndex 返回 -1
if ((index = _dictKeyIndex(d, key)) == -1)
return NULL;

// 被省略的代码...
}

然后 _dictKeyIndex 会在计算 地址索引前,会先调用 _dictExpandIfNeeded 检查两个哈希表是否有空间容纳新元素:
static int _dictKeyIndex(dict *d, const void *key)
{
// 被省略的代码...

/* Expand the hashtable if needed */
if (_dictExpandIfNeeded(d) == DICT_ERR)
return -1;

// 被省略的代码...
}

到 _dictExpandIfNeeded 这步,一些有趣的事情就开始发生了, _dictExpandIfNeeded 会检测到 0 号哈希表还没有分配任何空间,于是它调用 dictExpand ,传入 DICT_HT_INITIAL_SIZE 常量作为 0 号哈希表的初始大小(目前的版本 DICT_HT_INITIAL_SIZE = 4 ),为 0 号哈希表分配空间:
static int _dictExpandIfNeeded(dict *d)
{
// 被忽略的代码...

/* If the hash table is empty expand it to the intial size. */
if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);

// 被忽略的代码...
}

dictExpand 会创建一个分配了链表数组的新哈希表,然后进行判断,决定是该将新哈希表赋值给 0 号哈希表还是 1 号哈希表。
这里因为我们的 0 号哈希表的 size 还是 0 ,因此,这里会执行 if 语句的第一个 case ,将新哈希表赋值给 0 号哈希表:
int dictExpand(dict *d, unsigned long size)
{
// 被省略的代码...

// 计算哈希表的(真正)大小
unsigned long realsize = _dictNextPower(size);

// 创建新哈希表
dictht n;
n.size = realsize;
n.sizemask = realsize-1;
n.table = zcalloc(realsize*sizeof(dictEntry*)); // 分配链表数组
n.used = 0;

// 字典的 0 号哈希表是否已经初始化?
// 如果没有的话,我们将新建哈希表作为字典的 0 号哈希表
if (d->ht[0].table == NULL) {
d->ht[0] = n;
} else {
// 否则,将新建哈希表作为字典的 1 号哈希表,并将它用于 rehash
d->ht[1] = n;
d->rehashidx = 0;
}

// 被省略的代码...
}

字典的扩展和 1 号哈希表的创建

在 0 号哈希表创建之后,我们就有了一个可以执各式各样操作(添加、删除、查找,诸如此类)的字典实例了。

但是这里还有一个问题: 这个最初创建的 0 号哈希表非常小(当前版本的 DICT_HT_INITIAL_SIZE = 4),它很快就会被元素填满,这时候, rehash 操作就会被激活。

时间: 2024-09-29 08:37:19

字典:dict.c/dict.h的相关文章

python中将普通对象作为 字典类(dict) 使用

目前我知道的有两种方法: 1 定义的类继承dict类 例如 class A(dict): pass a = A() a['name'] = 12 2 给自定义的类添加 __setitem__() __getitem__()方法 class A: def __init__(self, cfg={}): <strong><span style="color:#ff0000;">self.cfg = cfg</span></strong> de

[转载]python中将普通对象作为 字典类(dict) 使用

目前我知道的有两种方法: 1 定义的类继承dict类 例如 class A(dict): pass a = A() a['name'] = 12 2 给自定义的类添加 __setitem__() __getitem__()方法 class A: def __init__(self, cfg={}): self.cfg = cfg def __setitem__(self, key, value): self.cfg[key] = value def __getitem__(self, key):

字典 的使用 dict

一.字典介绍:是python中最强大数据类型之一,什么是字典.字典的创建.赋值.字典的基本操作.映射类型操作.映射相关的函数. 1.什么是字典? 字典是python语言中唯一的映射类型. 映射类型对象里哈希值(键,key)和指向的对象(值,value)是一对多的关系, 字典的对象是可变的,它是一个容器类型,能存储任意个数的python对象, 字典类型与序列类型的区别: 1.存取和访问数组的方式不同. 2.序列类型只用数字类型的键(从序列的开始按数值顺序索引) 3.映射类型可以用其他对象类型做键(

dict.get &amp; dict.setdefault

当字典的值是复合类型时,使用dict.setdefault方法不要使用dict.get>>> d={}>>> d.setdefault('a',[]).append('A')>>> d.setdefault('a',[]).append('A1')>>> d{'a': ['A', 'A1']} >>> d.clear() >>> d['a']=d.get('a',0)+1>>> d

redis底层数据结构之dict 字典1

最近,我想通过redis的源码来学习redis.虽然平时工作中用得不多,不过对redis还是比较感兴趣的,毕竟它的性能是不错的.redis是一个 开源的项目,我们可以通过源代码去了解redis.我后面会通过自己的学习,写一些关于redis源码的帖子.帖子的主要内容是分析代码设计,而并不会对 源码进行详细解说.如果有不对的地方,请指正.源码是reids 3.0.3版本. dict 字典 一.数据结构 //字典条目 typedef struct dictEntry {     void *key;

python字典构造函数dict(mapping)解析

Python字典的构造函数有三个,dict().dict(**args).dict(mapping),当中第一个.第二个构造函数比較好理解也比較easy使用, 而dict(mapping)这个构造函数的使用比較难理解. 1 dict()构造函数能够返回一个空的字典 In [7]: d = dict() In [8]: print d {} In [9]: 2 dict(**arg)构造函数,传入參数用赋值表达式,可多个赋值表达式.用逗号间隔就可以. In [9]: d = dict(a = 12

【Redis源码剖析】 - Redis内置数据结构之字典dict

原创作品,转载请标明:http://blog.csdn.net/Xiejingfa/article/details/51018337 今天我们来讲讲Redis中的哈希表.哈希表在C++中对应的是map数据结构,但在Redis中称作dict(字典).Redis只是用了几个简单的结构体和几种常见的哈希算法就实现了一个简单的类似高级语言中的map结构.下面我们来具体分析一下dict的实现. 在学习数据结构的时候,我们接触过一种称作"散列表"的结构,可以根据关键字而直接访问记录.说的具体一点就

Python 基础之字典(dict)的用法

python dict字典字典是Python中唯一的内建映射类型,字典中的数据没有特定的顺序,可以通过特定的键(key)来存取数据.键可以是数字.元组或字符串,但不能是列表. 字典的常用方法:1.1 clear()方法1.2 copy()方法1.3 fromkeys()方法1.4 get()方法1.5 has_key()方法1.6 items.iteritems方法1.7 keys.iterkeys1.8 popitem()方法1.9 setdefault()方法1.10 update()方法1

字典dict

字典的每个键值(key=>value)对用冒号(:)分割,每个对之间用逗号(,)分割,整个字典包括在花括号({})中 ,格式如下所示: d = {key1 : value1, key2 : value2 } 字典是无序的:键必须是唯一的,但值则不必(值可以取任何数据类型,但键key必须是不可变的,如字符串,数字或元组,但列表就不行) 不允许同一个键出现两次.创建时如果同一个键被赋值两次,后一个值会被记住 1.访问字典的值 可以使用方括号[key] 2.修改字典的值 dict = {'Name':