Python2词典中的非单调内存消耗

时间:2016-04-30 10:58:48

标签: python memory cpython python-internals

有人可以解释CPython 2.7中字典的非单调内存使用吗?

>>> import sys
>>> sys.getsizeof({})
280
>>> sys.getsizeof({'one': 1, 'two': 2, 'three': 3, 'four': 4, 'five': 5})
280
>>> sys.getsizeof({'one': 1, 'two': 2, 'three': 3, 'four': 4, 'five': 5, 'six': 6})
1048
>>> sys.getsizeof({'one': 1, 'two': 2, 'three': 3, 'four': 4, 'five': 5, 'six': 6, 'seven': 7})
1048
>>> sys.getsizeof({'one': 1, 'two': 2, 'three': 3, 'four': 4, 'five': 5, 'six': 6, 'seven': 7, 'e
ight': 8})
664
>>> sys.getsizeof({'one': 1, 'two': 2, 'three': 3, 'four': 4, 'five': 5, 'six': 6, 'seven': 7, 'e
ight': 8, 'nine': 9})
664

Python3在这里是合理的,它将{'one': 1, 'two': 2, 'three': 3, 'four': 4, 'five': 5, 'six': 6, 'seven': 7}的大小打印为480.

我在Ubuntu 15.10和OS X 10.11上试过这个。

2 个答案:

答案 0 :(得分:1)

TLDR:6条和7条dict文字严重地压缩哈希表,然后调整大小四倍。

当CPython 2.7评估dict文字时,在开始填入条目之前,它用于创建dict的操作码是BUILD_MAP。这需要一个参数,一个提示dict将包含多少条目,which it uses to presize the dict

    TARGET(BUILD_MAP)
    {
        x = _PyDict_NewPresized((Py_ssize_t)oparg);
        PUSH(x);
        if (x != NULL) DISPATCH();
        break;
    }

这是为了最大限度地减少dict在创建过程中调整大小的次数,但由于它们没有考虑到加载因子,因此不会完全消除调整大小。

正如source code comments所示,_PyDict_NewPresized旨在“创建一个预先调整大小以容纳估计数量的元素的新字典”。创建的dict中哈希表的确切大小受许多实现细节的影响,例如最小大小(#define PyDict_MINSIZE 8)以及大小为2的幂的要求(以避免需要在实现)。

对于最多7个条目的dict文字,_PyDict_NewPresized初始化一个8项哈希表;对于8个条目,它初始化一个16项哈希表,因为它使用的调整大小例程总是选择一个大于参数的容量。

Dicts resize on insertion when they become at least 2/3 full.对于6和7条目的dict文字,dict以8个条目开始,因此在第6次插入时会发生调整大小。 dict足够小,调整大小使哈希表的大小增加四倍:

return dictresize(mp, (mp->ma_used > 50000 ? 2 : 4) * mp->ma_used);

mp->ma_used是哈希表中使用的条目数,此时为6。 6小于50000,所以我们调用dictresize(mp, 4 * 6),它将哈希表的大小调整为32个条目,2的最小幂大于24。

相比之下,对于8项dict文字,哈希表以16个条目开始。 dict在创建过程中不会变为2/3,因此最初的16项哈希表在dict创建中存活,并且得到的dict小于6和7项dict文字。

Python 3使用different growth policy以及其他dict实现更改,这就是您在Python 3中看到不同结果的原因。

答案 1 :(得分:0)

我已经尝试了一下,让我们看看:

dct = {'four': 3, 'three': 2, 'two': 1, 'one': 0}
print(sys.getsizeof(dct))                             # = 272
print(sys.getsizeof(dict(dct)))                       # = 272
print(sys.getsizeof({k: v for k, v in dct.items()}))  # = 272

dct = {'four': 3, 'three': 2, 'five': 4, 'two': 1, 'one': 0}
print(sys.getsizeof(dct))                             # = 272
print(sys.getsizeof(dict(dct)))                       # = 272
print(sys.getsizeof({k: v for k, v in dct.items()}))  # = 272

dct = {'six': 5, 'three': 2, 'two': 1, 'four': 3, 'five': 4, 'one': 0}
print(sys.getsizeof(dct))                             # = 1040
print(sys.getsizeof(dict(dct)))                       # = 656
print(sys.getsizeof({k: v for k, v in dct.items()}))  # = 1040

dct = {'seven': 6, 'six': 5, 'three': 2, 'two': 1, 'four': 3, 'five': 4, 'one': 0}
print(sys.getsizeof(dct))                             # = 1040
print(sys.getsizeof(dict(dct)))                       # = 656
print(sys.getsizeof({k: v for k, v in dct.items()}))  # = 1040

dct = {'seven': 6, 'six': 5, 'three': 2, 'two': 1, 'four': 3, 'five': 4, 'eight': 7, 'one': 0}
print(sys.getsizeof(dct))                             # = 656
print(sys.getsizeof(dict(dct)))                       # = 1040
print(sys.getsizeof({k: v for k, v in dct.items()}))  # = 1040

我不确定这里发生了什么样的优化,但我认为这是因为这些结构使用了不同的"最佳实践"。我的意思是什么时候为哈希表分配多少内存。例如,如果您有11个或更多元素,则会出现另一个奇怪的差异:

dct = {1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, 10:10, 11:11}
print(sys.getsizeof(dct))                             # = 1808
print(sys.getsizeof(dict(dct)))                       # = 1040
print(sys.getsizeof({k: v for k, v in dct.items()}))  # = 1040

所以这可能只是某种内存消耗"优化"当以不同的方式创建字典时,为什么在使用6或7个元素时,字面语法有这种非单调的异常值:我不知道。也许一些内存优化出错并且它分配了太多内存的Bug?我还没有读过源代码。