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

iul*_*ian 25 python dictionary python-2.7 python-3.x python-internals

我遇到过一种(非常不寻常的)情况,我不得不使用一个map()或一个列表推导表达式.然后我想知道哪一个更快.

这个 StackOverflow答案为我提供了解决方案,但后来我开始自己测试.基本上结果是一样的,但是我在切换到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
Run Code Online (Sandbox Code Playgroud)

我的假设是Python 3的是比Python 2快,但它在几个帖子横空出世(1,2),它的情况并非如此.然后我想也许Python 3.5在这么简单的任务中表现得更好,因为他们在下面说明README:

语言大致相同,但许多细节,特别是字典和字符串等内置对象的工作方式发生了很大变化,并且最终删除了许多已弃用的功能.

但不,它表现得更糟:

? 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
Run Code Online (Sandbox Code Playgroud)

我试图深入研究Python 3.5的源代码dict,但是我对C语言的了解并不足以自己找到答案(或者,我甚至不会在正确的地方搜索).

所以,我的问题是:

是什么让较新版本的Python与较旧版本的Python相比,在一个相对简单的任务(如dict定义)上更慢,因为通常意义上它反之亦然?我知道这些差异非常小,在大多数情况下它们可以忽略不计.这只是一个观察让我好奇为什么时间增加而至少不是一样的?

Kev*_*vin 31

因为没有人关心

您引用的差异大约为几十或几百纳秒.C编译器优化寄存器使用的方式略有不同,很容易引起这种变化(与任何其他C级优化差异一样).反过来,这可能是由许多事情引起的,例如Python的C实现(CPython)中局部变量的数量和使用的变化,甚至只是切换C编译器.

事实是,没有人积极优化这些微小的差异,所以没有人能够给你一个具体的答案.CPython的设计并不是绝对意义上的快速.它旨在可扩展.因此,例如,您可以将数百或数千个项目推送到字典中,并且它将继续表现良好.但是创建字典的绝对速度并不是Python实现者的主要关注点,至少当差异很小时.

  • @iulian:没有人试图让它保持不变,所以它升起并不足为奇.如果它失败也同样不足为奇.很多东西在2.7和3.5之间变化. (14认同)
  • @Kevin我认为这在技术上是正确的(最好的正确!)但你的回答的主旨我认为错误地表示时差和实施变化的规模.这没有发生,因为他们使用了几个本地变量. (3认同)

Mos*_*oye 21

正如@Kevin已经说过:

CPython的设计并不是绝对意义上的快速.它旨在可扩展

试试这个:

$ 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
Run Code Online (Sandbox Code Playgroud)

然后再次:

$ 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
Run Code Online (Sandbox Code Playgroud)

这很漂亮地表明,你不能将Python3作为对这些微不足道的差异而对Python2的损失.从外观上看,Python3应该更好地扩展.


Tom*_*Rup 18

让我们拆解 {}:

>>> from dis import dis
>>> dis(lambda: {})
  1           0 BUILD_MAP                0
              3 RETURN_VALUE
Run Code Online (Sandbox Code Playgroud)

Python 2.7实现BUILD_MAP

TARGET(BUILD_MAP)
{
    x = _PyDict_NewPresized((Py_ssize_t)oparg);
    PUSH(x);
    if (x != NULL) DISPATCH();
    break;
}
Run Code Online (Sandbox Code Playgroud)

Python 3.5实现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();
}
Run Code Online (Sandbox Code Playgroud)

这是更多的代码.

编辑:

Python 3.4实现的BUILD_MAP id与2.7完全相同(感谢@ user2357112).我深入挖掘它看起来像Python 3分钟大小的dict是8 PyDict_MINSIZE_COMBINED const

PyDict_MINSIZE_COMBINED是任何新的非拆分字典的起始大小.8允许dicts不超过5个活动条目; 实验表明,这足以满足大多数dicts(主要包括为传递关键字参数而创建的通常小的dicts).使这8而不是4减少了大多数字典的调整大小,没有任何显着的额外内存使用.

在Python 3.4中查看_PyDict_NewPresized

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);
}
Run Code Online (Sandbox Code Playgroud)

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;
}
Run Code Online (Sandbox Code Playgroud)

在这两种情况下minused都有价值1.

Python 2.7创建一个空字典,Python 3.4创建一个7元素字典.

  • 两个额外的高度可预测的分支似乎不会导致观察到的影响.对于`{}`情况,它需要3倍的时间!已经涉及的所有分支和分配中的另外两个分支不会这样做.对于`dict()`情况,这个代码都没有运行,但绝对时间增加与`{}`情况大致相同.要找到减速的实际根,您需要深入挖掘. (4认同)
  • 实际上没有循环运行,因为堆栈上没有参数.3.x代码*与2.x代码没有实质性的不同*除了一些额外的未采用分支和"PUSH(map); 讯();`. (3认同)
  • 另外,你指出的差异[甚至不存在于3.4](https://hg.python.org/cpython/file/3.4/Python/ceval.c#l2377); 他们是一个3.5变化. (2认同)