之前的答案已经提到你不必担心,所以我将深入研究一些更多的技术细节。有点长,请多多包涵。
TLDR:这与调整大小的算法有关。每次调整大小都会分配2**i 内存,其中2**i > requested_size; 2**i >= 8,但是如果2/3 的插槽被填满,则每次插入都会进一步调整基础表的大小,但这次是new_size = old_size * 4。这样,您的第一个字典最终分配了 32 个单元格,而第二个字典分配了 16 个单元格(因为它预先获得了更大的初始大小)。
答案:正如@snakecharmerb 在 cmets 中指出的,这取决于字典的创建方式。为简洁起见,我建议您参考this, excellent blog post,它解释了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 个元素;不要担心这个,因为这个特定的优化对我们来说并不像看起来那么重要。
现在,当您使用 dict 文字 {} 创建字典时,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;
}
此函数调用普通字典构造函数 (PyDict_New) 并请求调整新创建的字典的大小 - 但前提是它预计包含 5 个以上的元素。这是由于一项优化允许 Python 通过将数据保存在预分配的“小表”中来加速某些事情,而无需调用昂贵的内存分配和取消分配函数。
然后,dictresize 将尝试确定新字典的最小大小。它还将使用幻数 8 - 作为起点并迭代地乘以 2,直到找到大于请求大小的最小大小。对于第一个字典,这只是 8,然而,对于第二个(以及所有由 dict 字面量创建的少于 15 个键的字典),它是 16。
现在,在dictresize函数中,前者有a special case,更小的new_size == 8,意在提出上述优化(使用“小表”减少内存操作操作)。但是,因为不需要调整新创建的 dict 的大小(例如,到目前为止没有删除任何元素,因此表是“干净的”),所以什么都没有发生。
相反,当new_size != 8 时,重新分配哈希表的通常过程如下。这最终会分配一个新表来存储
“大”词典。虽然这很直观(更大的 dict 有更大的表),但这似乎并没有让我们继续观察到的行为 - 但是,请再忍耐一下。
一旦我们有了预分配的字典,STORE_MAP optcodes 就会告诉解释器插入连续的键值对。这是通过dict_set_item_by_hash_or_entry 函数实现的,重要的是,如果超过 2/3 的插槽已经用完,则在每次增加大小(即成功插入)后调整字典的大小。大小将增加 x4(in our case,对于大型 dicts 仅增加 x2)。
因此,当您创建包含 7 个元素的 dict 时会发生以下情况:
# 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 个元素的 dict。
但是,当添加 8 个元素时,初始大小将增加两倍(16),因此我们永远不会调整大小,因为条件 ratio_filled > 2/3 永远不会满足!
这就是为什么在第二种情况下你最终会得到一张更小的桌子。