为什么 dict 定义在 Python 2.7 中比在 Python 3.x 中更快?

Why is dict definition faster in Python 2.7 than in Python 3.x?

我遇到过(不是很不寻常的)情况,在这种情况下我不得不使用 map() 或列表理解表达式。然后我想知道哪个更快。

This Whosebug 回答为我提供了解决方案,但后来我开始自己测试。基本上结果是一样的,但是我在切换到 Python 3 时发现了一个我很好奇的意外行为,即:

λ iulian-pc ~ → python --version
Python 2.7.6
λ iulian-pc ~ → python3 --version
Python 3.4.3

λ iulian-pc ~ → python -mtimeit '{}'                                                     
10000000 loops, best of 3: 0.0306 usec per loop
λ iulian-pc ~ → python3 -mtimeit '{}'                
10000000 loops, best of 3: 0.105 usec per loop

λ iulian-pc ~ → python -mtimeit 'dict()'
10000000 loops, best of 3: 0.103 usec per loop
λ iulian-pc ~ → python3 -mtimeit 'dict()'
10000000 loops, best of 3: 0.165 usec per loop

我曾假设 Python 3 比 Python 2 快,但在几篇文章 (1, 2) 中发现事实并非如此。然后我想也许 Python 3.5 会在这样一个简单的任务上表现得更好,正如他们在 README:

中所说的那样

The language is mostly the same, but many details, especially how built-in objects like dictionaries and strings work, have changed considerably, and a lot of deprecated features have finally been removed.

但是不,它的表现更差:

λ iulian-pc ~ → python3 --version
Python 3.5.0

λ iulian-pc ~ → python3 -mtimeit '{}'       
10000000 loops, best of 3: 0.144 usec per loop
λ iulian-pc ~ → python3 -mtimeit 'dict()'
1000000 loops, best of 3: 0.217 usec per loop

我试图深入研究 dict 的 Python 3.5 源代码,但我对 C 语言的了解不足以自己找到答案(或者,也许我什至不知道不要在正确的地方搜索)。

那么,我的问题是:

是什么让新版本的 Python 与旧版本的 Python 相比,在相对简单的任务(例如 dict 定义上,按照常识应该如此反之亦然?我知道这些差异非常小,在大多数情况下可以忽略不计。这只是一个观察让我好奇为什么时间增加而不是至少保持不变?

因为没人关心

您引用的差异大约是几十或几百纳秒。 C 编译器如何优化寄存器使用的细微差别很容易导致此类更改(任何数量的其他 C 级优化差异也可能如此)。反过来,这可能是由许多事情引起的,例如 Python (CPython) 的 C 实现中局部变量的数量和用法的变化,甚至只是切换 C 编译器.

事实是,没有人会积极优化这些小差异,因此没有人能够给您一个具体的答案。 CPython 并不是为了绝对意义上的快速而设计的。它被设计为 可扩展 。因此,例如,您可以将成百上千个项目塞入字典,它会继续表现良好。但是创建字典的绝对速度根本不是 Python 实现者的主要关注点,至少在差异这么小的时候是这样。

正如@Kevin 所说:

CPython is not designed to be fast in an absolute sense. It is designed to be scalable

试试这个:

$ python -mtimeit "dict([(2,3)]*10000000)"
10 loops, best of 3: 512 msec per loop
$
$ python3 -mtimeit "dict([(2,3)]*10000000)"
10 loops, best of 3: 502 msec per loop

再一次:

$ python -mtimeit "dict([(2,3)]*100000000)"
10 loops, best of 3: 5.19 sec per loop
$
$ python3 -mtimeit "dict([(2,3)]*100000000)"
10 loops, best of 3: 5.07 sec per loop

这很好地表明您不能将 Python3 视为输给 Python2 如此微不足道的差异。从表面上看,Python3 应该可以更好地扩展。

让我们disassemble{}:

>>> from dis import dis
>>> dis(lambda: {})
  1           0 BUILD_MAP                0
              3 RETURN_VALUE

Python 2.7 implementation of BUILD_MAP

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

Python 3.5 implementation of BUILD_MAP

TARGET(BUILD_MAP) {
    int i;
    PyObject *map = _PyDict_NewPresized((Py_ssize_t)oparg);
    if (map == NULL)
        goto error;
    for (i = oparg; i > 0; i--) {
        int err;
        PyObject *key = PEEK(2*i);
        PyObject *value = PEEK(2*i - 1);
        err = PyDict_SetItem(map, key, value);
        if (err != 0) {
            Py_DECREF(map);
            goto error;
        }
    }

    while (oparg--) {
        Py_DECREF(POP());
        Py_DECREF(POP());
    }
    PUSH(map);
    DISPATCH();
}

代码有点多。

编辑:

Python 3.4 实现 BUILD_MAP id 与 2.7 完全相同(感谢 @user2357112)。我更深入地挖掘,它看起来像 Python 3 min size of dict is 8 PyDict_MINSIZE_COMBINED const

PyDict_MINSIZE_COMBINED is the starting size for any new, non-split dict. 8 allows dicts with no more than 5 active entries; experiments suggested this suffices for the majority of dicts (consisting mostly of usually-small dicts created to pass keyword arguments). Making this 8, rather than 4 reduces the number of resizes for most dictionaries, without any significant extra memory use.

看看_PyDict_NewPresized in Python 3.4

PyObject *
_PyDict_NewPresized(Py_ssize_t minused)
{
    Py_ssize_t newsize;
    PyDictKeysObject *new_keys;
    for (newsize = PyDict_MINSIZE_COMBINED;
         newsize <= minused && newsize > 0;
         newsize <<= 1)
        ;
    new_keys = new_keys_object(newsize);
    if (new_keys == NULL)
        return NULL;
    return new_dict(new_keys, NULL);
}

并在 2.7

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;
}

在这两种情况下 minused 的值为 1。

Python 2.7 创建一个空字典和 Python 3.4 创建一个 7 元素字典。