众所周知,python3.6这个版本对dict的实现是做了较大优化的,特别是在内存使用率方面,因此我觉得有必要研究一下最新的dict的源码实现。

前后断断续续看了大概一周多一点,主要在研究dict和创建实例对象那部分的代码,在此将所得记录下来。

值得一提的事,新版的dict使用的算法还是一样的,比如说hash值计算、冲突解决策略(open addressing)等。因此这一部分也不是我关注的重点,我关注的主要是在新的dict如何降低内存使用这方面。

btw,本文的分析是基于python的3.6.1这个版本。

话不多说,先看 PyDictObject 结构的定义:

 typedef struct _dictkeysobject PyDictKeysObject;

 /* The ma_values pointer is NULL for a combined table
* or points to an array of PyObject* for a split table
*/
typedef struct {
PyObject_HEAD /* Number of items in the dictionary */
Py_ssize_t ma_used; /* Dictionary version: globally unique, value change each time
the dictionary is modified */
uint64_t ma_version_tag; PyDictKeysObject *ma_keys; /* If ma_values is NULL, the table is "combined": keys and values
are stored in ma_keys. If ma_values is not NULL, the table is splitted:
keys are stored in ma_keys and values are stored in ma_values */
PyObject **ma_values;
} PyDictObject;

说下新增的 PyDictKeysObject 这个对象,其定义如下:

 /* See dictobject.c for actual layout of DictKeysObject */
struct _dictkeysobject {
Py_ssize_t dk_refcnt; /* Size of the hash table (dk_indices). It must be a power of 2. */
Py_ssize_t dk_size; /* Function to lookup in the hash table (dk_indices): - lookdict(): general-purpose, and may return DKIX_ERROR if (and
only if) a comparison raises an exception. - lookdict_unicode(): specialized to Unicode string keys, comparison of
which can never raise an exception; that function can never return
DKIX_ERROR. - lookdict_unicode_nodummy(): similar to lookdict_unicode() but further
specialized for Unicode string keys that cannot be the <dummy> value. - lookdict_split(): Version of lookdict() for split tables. */
dict_lookup_func dk_lookup; /* Number of usable entries in dk_entries. */
Py_ssize_t dk_usable; /* Number of used entries in dk_entries. */
Py_ssize_t dk_nentries; /* Actual hash table of dk_size entries. It holds indices in dk_entries,
or DKIX_EMPTY(-1) or DKIX_DUMMY(-2). Indices must be: 0 <= indice < USABLE_FRACTION(dk_size). The size in bytes of an indice depends on dk_size: - 1 byte if dk_size <= 0xff (char*)
- 2 bytes if dk_size <= 0xffff (int16_t*)
- 4 bytes if dk_size <= 0xffffffff (int32_t*)
- 8 bytes otherwise (int64_t*) Dynamically sized, 8 is minimum. */
union {
int8_t as_1[];
int16_t as_2[];
int32_t as_4[];
#if SIZEOF_VOID_P > 4
int64_t as_8[];
#endif
} dk_indices; /* "PyDictKeyEntry dk_entries[dk_usable];" array follows:
see the DK_ENTRIES() macro */
};

新版的dict在内存布局上和旧版有了很大的差异,其中一点就是分离存储了key和value。设计思路可以看看这个:More compact dictionaries with faster iteration

还有一点需要说明的是,新版的dict有两种形式,分别是 combined 和 split。其中后者主要用在优化对象存储属性的tp_dict上,这个在后面讨论。

对于旧版的hash table,其每个slot存储的是一个 PyDictKeyEntry 对象(PyDictKeyEntry是一个三元组,包含了hash、key、value),这样带来的问题就是,多占用了一些非必要的内存。对于状态为EMPTY的slot,实际可能存储为(0,NULL,NULL)这种形式,但其实这些数据都是冗余的。

因此新版的hash table对此作出了优化,slot(也即是 dk_indices) 存储的不再是一个 PyDictKeyEntry,而是一个数组的index,这个数组存储了具体且必要的 PyDictKeyEntry对象 。对于那些EMPTY、DUMMY状态的这类slot,只需要用个负数(区分大于0的index)表示即可。

实际上,优化还不止于此。实际上还会根据需要索引 PyDictKeyEntry 对象的数量,动态的决定是用什么类型的变量来表示index。例如,如果所存储的 PyDictKeyEntry 数量不超过127,那么实际上用长度为一个字节的带符号整数(char)存储index即可。需要说明的是,index的值是有可能为负的(EMPTY、DUMMY、ERROR),因此需要用带符号的整数存储。具体可以看 new_keys_object 这个函数,这个函数在创建 dict 的时候会被调用:

 PyObject *
PyDict_New(void)
{
PyDictKeysObject *keys = new_keys_object(PyDict_MINSIZE);
if (keys == NULL)
return NULL;
return new_dict(keys, NULL);
} static PyDictKeysObject *new_keys_object(Py_ssize_t size)
{
PyDictKeysObject *dk;
Py_ssize_t es, usable; assert(size >= PyDict_MINSIZE);
assert(IS_POWER_OF_2(size)); usable = USABLE_FRACTION(size);
if (size <= 0xff) {
es = ;
}
else if (size <= 0xffff) {
es = ;
}
#if SIZEOF_VOID_P > 4
else if (size <= 0xffffffff) {
es = ;
}
#endif
else {
es = sizeof(Py_ssize_t);
} if (size == PyDict_MINSIZE && numfreekeys > ) {
dk = keys_free_list[--numfreekeys];
}
else {
dk = PyObject_MALLOC(sizeof(PyDictKeysObject)
- Py_MEMBER_SIZE(PyDictKeysObject, dk_indices)
+ es * size
+ sizeof(PyDictKeyEntry) * usable);
if (dk == NULL) {
PyErr_NoMemory();
return NULL;
}
}
DK_DEBUG_INCREF dk->dk_refcnt = ;
dk->dk_size = size;
dk->dk_usable = usable;
dk->dk_lookup = lookdict_unicode_nodummy;
dk->dk_nentries = ;
memset(&dk->dk_indices.as_1[], 0xff, es * size);
memset(DK_ENTRIES(dk), , sizeof(PyDictKeyEntry) * usable);
return dk;
}

有几点需要说明一下:

(1)受限于装填因子,因此给定一个hash table 的 size 就能确定出最多可容纳多少个有效对象(上图代码18行),因此存储的 PyDictKeyEntry 对象的数组的长度是可以在一开始便确定下来的。PyDictKeysObject 对象上的 dk_usable 表示hash table还能存储多少个对象,其值小于等于0的时候,再插入元素需要执行 rehash 操作。

(2)传入的size的值必须是2的幂,因此如果 size <= 0xff(255) 成立,则说明 size <= 128,因此用1个字节长度来表示index足矣。

(3)CPython的代码到处存在着缓存策略,keys_free_list 也是如此,目的是减少实际执行malloc的次数。

(4)当申请内存时,在计算一个 PyDictKeysObject 对象实际需要的内存时,需要减去 dk_indices 成员默认的大小,默认大小是8字节。这部分内存是根据size动态确定下来的。

现在来说说之前提及的split形式的dict。这种字典的key是共享的,有一个引用计数器 dk_refcnt 来维护当前被引用的个数。而之所以设计出split形式的字典,是因为观察到了python虚拟机中,会有大量key相同而value不同的字典的存在。而这个特定的情况就是实例对象上存储属性的 tp_dict 字典!

因此split形式的dict主要是出于对优化实例对象上存储属性这种情况考虑的。设计思路这里有所提及:PEP 412 -- Key-Sharing Dictionary

我们都知道,python使用dict来存储对象的属性。考虑一个这样的场景:

(1)一个类会创建出很多个对象。

(2)这些对象的属性,能在一开始就确定下来,并且后续不会增加删除。

如果能满足上述两个条件,那么其实我们可以使用一种更高效、更省内存的方式,来存储对象的属性。方法就是,属于一个类的所有对象共享同一份属性字典的key,而value以数组的方式存储在每个对象的身上。优化的好处是显而易见的,原来需要为每一个对象维持一份属性key,而现在只需为所有对象维持一份即可,并且属性的值(value)也以更加紧凑的方式组织在内存中。新版的dict的设计使得实现这种共享key的策略变得更简单!

看看具体的代码:

 int
_PyObjectDict_SetItem(PyTypeObject *tp, PyObject **dictptr,
PyObject *key, PyObject *value)
{
PyObject *dict;
int res;
PyDictKeysObject *cached; assert(dictptr != NULL);
if ((tp->tp_flags & Py_TPFLAGS_HEAPTYPE) && (cached = CACHED_KEYS(tp))) {
assert(dictptr != NULL);
dict = *dictptr;
if (dict == NULL) {
DK_INCREF(cached);
dict = new_dict_with_shared_keys(cached); // importance!!!
if (dict == NULL)
return -;
*dictptr = dict;
}
if (value == NULL) {
res = PyDict_DelItem(dict, key);
// Since key sharing dict doesn't allow deletion, PyDict_DelItem()
// always converts dict to combined form.
if ((cached = CACHED_KEYS(tp)) != NULL) {
CACHED_KEYS(tp) = NULL;
DK_DECREF(cached);
}
}
else {
int was_shared = (cached == ((PyDictObject *)dict)->ma_keys);
res = PyDict_SetItem(dict, key, value);
if (was_shared &&
(cached = CACHED_KEYS(tp)) != NULL &&
cached != ((PyDictObject *)dict)->ma_keys) {
/* PyDict_SetItem() may call dictresize and convert split table
* into combined table. In such case, convert it to split
* table again and update type's shared key only when this is
* the only dict sharing key with the type.
*
* This is to allow using shared key in class like this:
*
* class C:
* def __init__(self):
* # one dict resize happens
* self.a, self.b, self.c = 1, 2, 3
* self.d, self.e, self.f = 4, 5, 6
* a = C()
*/
if (cached->dk_refcnt == ) {
CACHED_KEYS(tp) = make_keys_shared(dict);
}
else {
CACHED_KEYS(tp) = NULL;
}
DK_DECREF(cached);
if (CACHED_KEYS(tp) == NULL && PyErr_Occurred())
return -;
}
}
} else {
dict = *dictptr;
if (dict == NULL) {
dict = PyDict_New();
if (dict == NULL)
return -;
*dictptr = dict;
}
if (value == NULL) {
res = PyDict_DelItem(dict, key);
} else {
res = PyDict_SetItem(dict, key, value);
}
}
return res;
}
当我们在类的 __init__ 方法中通过 self.a = v 初始化一个对象的属性时,最终会调用到函数_PyObjectDict_SetItem。此函数会初始化对象的tp_dict,也即是对象的属性字典。从上述的第15行代码可以看出,在特定情况下,会将对象的属性字典初始化为共享key的split式字典。因此也验证了之前的分析。

Python3中对Dict的内存优化的更多相关文章

  1. Android 中对于图片的内存优化方法

    Android 中对于图片的内存优化方法,需要的朋友可以参考一下     1. 对图片本身进行操作 尽量不要使用 setImageBitmap.setImageResource. BitmapFact ...

  2. Python内存优化:Profile,slots,compact dict

    实际项目中,pythoner更加关注的是Python的性能问题,之前也写过一篇文章<Python性能优化>介绍Python性能优化的一些方法.而本文,关注的是Python的内存优化,一般说 ...

  3. Python内存优化

    实际项目中,pythoner更加关注的是Python的性能问题,之前也写过一篇文章<Python性能优化>介绍Python性能优化的一些方法.而本文,关注的是Python的内存优化,一般说 ...

  4. 试试SQLSERVER2014的内存优化表

    试试SQLSERVER2014的内存优化表 SQL Server 2014中的内存引擎(代号为Hekaton)将OLTP提升到了新的高度. 现在,存储引擎已整合进当前的数据库管理系统,而使用先进内存技 ...

  5. android内存优化相关1

    第一种策略,是释放显示相关的内存.这是我们针对系统APP采用的一种调优策略. 图形内容,俗称位图是非常占用内存的,针对位图,我们采用异步加载的方法,将位图内容信息和位图的状态信息分别进行存储,将内容信 ...

  6. SQLSERVER2014的内存优化表

    SQL Server 2014中的内存引擎(代号为Hekaton)将OLTP提升到了新的高度. 现在,存储引擎已整合进当前的数据库管理系统,而使用先进内存技术来支持大规模OLTP工作负载. 就算如此, ...

  7. 试试SQLServer 2014的内存优化表

    SQL Server2014存储引擎:行存储引擎,列存储引擎,内存引擎 SQL Server 2014中的内存引擎(代号为Hekaton)将OLTP提升到了新的高度. 现在,存储引擎已整合进当前的数据 ...

  8. 试试SQLServer 2014的内存优化表(转载)

    SQL Server2014存储引擎:行存储引擎,列存储引擎,内存引擎 SQL Server 2014中的内存引擎(代号为Hekaton)将OLTP提升到了新的高度. 现在,存储引擎已整合进当前的数据 ...

  9. 使用SQL Server内存优化表 In-Memory OLTP

    如果你的系统有高并发的要求,可以尝试使用SQL Server内存优化表来提升你的系统性能.你甚至可以把它当作Redis来使用. 要使用内存优化表,首先要在现在数据库中添加一个支持内存优化的文件组. M ...

随机推荐

  1. Python常忘的基础知识

    0.目录 1.进制 1.1 各进制的表示 1.2 各进制的转换 2.字符 2.1 转义字符 2.2 原始字符串 3.类型 3.1 基本数据类型 3.2 type() 4.变量与运算符 4.1 值类型与 ...

  2. Day19内容回顾

    1,Django请求的生命周期 路由系统-视图函数(获取模板+数据->渲染)->字符串返回给用户 2,路由系统 /index/ 函数或类.as_view() /detail(\d+)/ 函 ...

  3. 【CodeChef-SPCLN】Cleaning the Space

    https://odzkskevi.qnssl.com/7dfb262544887eff6fb35bfb444759d6?v=1502084197 做法是类似于最大割之类的东西,把每个碎片按照按钮拆点 ...

  4. 洛谷 P1924 poj 1038

    Description: 给你一个n * m的方格纸,有一些格子无法被覆盖,然后用2*3的格子覆盖这个方格纸,问你最多能放多少个格子 神级状压 为了弄清楚这道题翻了无数篇解题报告,最后终于搞明白了 用 ...

  5. 【期望】【P5081】Tweetuzki 爱取球

    Description Tweetuzki 有一个袋子,袋子中有 \(N\) 个无差别的球.Tweetuzki 每次随机取出一个球后放回.求取遍所有球的期望次数. 取遍是指,袋子中所有球都被取出来过至 ...

  6. 用dtree实现树形菜单 dtree使用说明

    http://www.jb51.net/article/28566.htm 准备工作: 请从脚本之家http://www.jb51.net/jiaoben/31974.html下载dtree.zip文 ...

  7. Spring Swagger URL传参问题(转)

    代码例子: @ApiOperation(value="获取用户详细信息", notes="根据url的id来获取用户详细信息") @ApiImplicitPar ...

  8. Educational Codeforces Round 24 A 水 B stl C 暴力 D stl模拟 E 二分

    A. Diplomas and Certificates time limit per test 1 second memory limit per test 256 megabytes input ...

  9. Codeforces 931.F Teodor is not a liar!

    F. Teodor is not a liar! time limit per test 1 second memory limit per test 256 megabytes input stan ...

  10. laravel5.1 关联模型保存的方法(使用associate方法)

    模型定义 class User { public function customer() { return $this->hasOne('Customer'); } } class Custom ...