字典大小随着增加一个元素而减小

时间:2019-05-26 11:46:02

标签: python python-2.7 dictionary

我跑了:

import sys

diii = {'key1':1,'key2':2,'key3':1,'key4':2,'key5':1,'key6':2,'key7':1}
print sys.getsizeof(diii)
# output: 1048

diii = {'key1':1,'key2':2,'key3':1,'key4':2,'key5':1,'key6':2,'key7':1,'key8':2}
print sys.getsizeof(diii)
# output: 664  

在这里询问之前,我重新启动了python shell并在线进行了尝试,并得到了相同的结果。
我认为一本包含一个以上元素的字典将提供与输出相同或更多的字节,而不是包含一个较少元素的字典。

知道我在做什么错吗?

5 个答案:

答案 0 :(得分:8)

先前的答案已经提到您不必担心,因此我将深入探讨一些技术细节。很长,但是请忍受我。

TLDR :这与调整大小的算法有关。每次调整大小都会分配2**i内存,其中2**i > requested_size; 2**i >= 8,但是如果插入了2/3的插槽,则每次插入都会进一步调整基础表的大小,但这一次new_size = old_size * 4。这样,您的第一个字典最终分配了32个单元格,而第二个字典仅分配了16个单元格(因为它的初始大小更大)。

答案:正如@snakecharmerb在评论中指出的那样,这取决于字典的创建方式。为了简洁起见,请允许我参考this, excellent blog post,它解释了dict()构造函数和dict文字{}在Python字节码和CPython实现级别上的区别。

让我们从8个数字的神奇数字开始。事实证明,它是dictobject.h头文件中为Python 2.7实现预定义的常量  -Python字典的最小大小:

/* PyDict_MINSIZE is the minimum size of a dictionary.  This many slots are
 * allocated directly in the dict object (in the ma_smalltable member).
 * It must be a power of 2, and at least 4.  8 allows dicts with no more
 * than 5 active entries to live in ma_smalltable (and so avoid an
 * additional malloc); instrumentation suggested this suffices for the
 * majority of dicts (consisting mostly of usually-small instance dicts and
 * usually-small dicts created to pass keyword arguments).
 */
#define PyDict_MINSIZE 8

因此,在特定的Python实现之间可能有所不同,但是让我们假设我们都使用相同的CPython版本。但是,大小为8的dict预计将仅包含5个元素;不必担心,因为这种特定的优化对我们而言似乎并不重要。

现在,当您使用字典文字{}创建字典时,CPython采用了快捷方式(与调用dict构造函数时的显式创建相比)。字节码操作BUILD_MAP简化了一点,它导致调用_PyDict_NewPresized函数,该函数将构造一个我们已经预先知道其大小的字典:

/* Create a new dictionary pre-sized to hold an estimated number of elements.
   Underestimates are okay because the dictionary will resize as necessary.
   Overestimates just mean the dictionary will be more sparse than usual.
*/

PyObject *
_PyDict_NewPresized(Py_ssize_t minused)
{
    PyObject *op = PyDict_New();

    if (minused>5 && op != NULL && dictresize((PyDictObject *)op, minused) == -1) {
        Py_DECREF(op);
        return NULL;
    }
    return op;
}

此函数调用普通的dict构造函数(PyDict_New)并请求调整新创建的dict的大小-但前提是希望它容纳5个以上的元素。这是由于一项优化,它允许Python通过将数据保存在预先分配的“小表”中来加速某些事情,而无需调用昂贵的内存分配和取消分配功能。

然后,dictresize将尝试确定新字典的最小大小。它还将使用魔术数8-作为起点,并迭代乘以2,直到找到最小大小大于请求的大小。对于第一个字典,它只是8,但是,对于第二个字典(以及由dict文字少于15个键的所有字典),它是16。

现在,在dictresize函数中,前一个较小的new_size == 8a special case,这是为了进行上述优化(使用“小表”来减少内存)操作)。但是,由于不需要调整新创建的dict的大小(例如,到目前为止尚未删除任何元素,因此表是“干净的”)。

相反,当new_size != 8时,遵循重新分配哈希表的常规过程。最后分配一个新表来存储 “大”字典。尽管这是直观的(较大的dict有较大的表),但这似乎尚未使我们前进到所观察到的行为-但是,请多忍一下。

一旦有了预分配的字典,STORE_MAP操作码就会告诉解释器插入连续的键值对。这是通过dict_set_item_by_hash_or_entry函数实现的,重要的是-如果已用完超过2/3的插槽,则每次增加大小(即成功插入)后都会调整字典的大小。大小将增加4倍({较大的字典仅增加2倍,in our case)。

因此,当您创建包含7个元素的字典时会发生以下情况:

# note 2/3 = 0.(6)
BUILD_MAP   # initial_size = 8, filled = 0
STORE_MAP   # 'key_1' ratio_filled = 1/8 = 0.125, not resizing
STORE_MAP   # 'key_2' ratio_filled = 2/8 = 0.250, not resizing
STORE_MAP   # 'key_3' ratio_filled = 3/8 = 0.375, not resizing
STORE_MAP   # 'key_4' ratio_filled = 4/8 = 0.500, not resizing
STORE_MAP   # 'key_5' ratio_filled = 5/8 = 0.625, not resizing
STORE_MAP   # 'key_6' ratio_filled = 6/8 = 0.750, RESIZING! new_size = 8*4 = 32
STORE_MAP   # 'key_7' ratio_filled = 7/32 = 0.21875

最后,您得到的字典在哈希表中的总大小为32个元素。

但是,当添加八个元素时,初始大小将增加两倍(16),因此我们将永远不会调整大小,因为条件ratio_filled > 2/3将永远不会得到满足!

这就是为什么在第二种情况下您最终得到一个较小的表的原因。

答案 1 :(得分:7)

sys.getsizeof返回分配给那些字典的基础哈希表实现的内存,该内存与字典的实际大小之间存在某种不太明显的关系。

Python 2.7的CPython实现每次将其填充到其哈希表容量的2/3时,分配给哈希表的内存量将增加四倍,但是如果它为内存分配的内存过多(即,一个较大的连续块)将其缩小内存已分配,但实际只使用了几个地址。

碰巧的是,具有8到11个元素的字典为CPython分配了足够的内存,以将其视为“过度分配”并收缩。

答案 2 :(得分:2)

您没有做错任何事情。字典的大小并不完全与元素的数量相对应,因为一旦使用了一定百分比的内存空间,字典就会被过度分配并动态调整大小。我不确定您的示例中是什么使dict缩小到2.7(而不是3),但是您不必担心它。为什么要使用2.7?为什么要知道dict的确切内存使用情况(顺便说一句,它不包括字典中包含的变量使用的内存,因为字典本身充满了指针。

答案 3 :(得分:1)

在这里处理字典文字的分配:dictobject.c#L685-L695

由于实现方式的怪异,大小与元素数之和最终不是monotonically increasing

import sys

def getsizeof_dict_literal(n):
    pairs = ["{0}:{0}".format(i) for i in range(n)]
    dict_literal = "{%s}" % ", ".join(pairs)
    source = "sys.getsizeof({})".format(dict_literal)
    size = eval(source)
    return size

表现出的奇怪的起伏收缩行为不仅是一次奇怪的一次性事故,而且是定期重复发生的事件。对于前几千个结果,可视化效果如下所示:

dict literal sizes py2

在Python的最新版本中,dict的实现完全不同,分配细节更加合理。有关最近更改的示例,请参见bpo28731 - _PyDict_NewPresized() creates too small dict。在Python 3.7.3中,可视化现在看起来像这样,总体上具有较小的dict和单调的分配:

dict literal sizes py3

答案 4 :(得分:-1)

您实际上没有做错任何事情。 auth1:login不能得到字典中元素的大小,但可以得到字典的粗略估计。解决此问题的另一种方法是使用getsizeof库中的json.dumps()。尽管它没有提供对象的实际大小,但与您对对象所做的更改是一致的。

这是一个例子

json

import sys import json diii = {'key1':1,'key2':2,'key3':1,'key4':2,'key5':1,'key6':2,'key7':1} print sys.getsizeof(json.dumps(diii)) # <---- diii = {'key1':1,'key2':2,'key3':1,'key4':2,'key5':1,'key6':2,'key7':1,'key8':2} print sys.getsizeof(json.dumps(diii)) # <---- 将字典更改为json字符串,然后可以将json.dumps()视为字符串。 详细了解python的diiihere