【问题标题】:Why is dict definition faster in Python 2.7 than in Python 3.x?为什么 Python 2.7 中的 dict 定义比 Python 3.x 中的更快?
【发布时间】:2016-09-26 21:50:07
【问题描述】:

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

ThisStackOverflow 的回答为我提供了解决方案,但后来我开始自己测试。基本上结果是一样的,但是我在切换到 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 快,但在几篇文章(12)中证明并非如此。然后我想也许 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

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

所以,我的问题是:

在相对简单的任务(例如 dict 定义)上,与旧版 Python 相比,新版 Python 的速度慢的原因是什么,按照常识,反之亦然?我知道这些差异是如此之小,以至于在大多数情况下它们可以被忽略。这只是一个观察,让我很好奇为什么时间增加了,至少没有保持不变?

【问题讨论】:

  • 请注意,dict() 是一个函数调用。这需要查找 dict 函数然后调用它,这显然比不需要查找的内置语法有更多的开销。
  • @Bakuriu,我知道,我只是为了表明文字和构造函数符号都有增加。
  • 现在继续寻找 python -m timeit 1 在 cpython 版本之间变化的原因。

标签: python python-2.7 python-3.x dictionary python-internals


【解决方案1】:

让我们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();
}

代码有点多。

编辑:

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

PyDict_MINSIZE_COMBINED 是任何新的非拆分字典的起始大小。 8 允许不超过 5 个活动条目的字典;实验表明这足以满足大多数 dicts(主要由创建用于传递关键字参数的通常较小的 dicts 组成)。将其设为 8 而不是 4 可以减少大多数字典的大小调整次数,而不会使用任何显着的额外内存。

_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 元素字典。

【讨论】:

  • 两个循环实际上都没有运行,因为堆栈上没有参数。 3.x 代码与 2.x 代码没有实质性不同,除了一些额外的未采用的分支和 PUSH(map); DISPATCH();
  • @Kevin 是的,但是3.x的代码有分支,必须分析什么时候做,什么时候不做。
  • 两个额外的高度可预测的分支似乎不会导致观察到的效果。对于{} 的情况,它需要 3 倍的时间!已经涉及的所有分支和分配中的另外两个分支不会这样做。对于dict() 的情况,这段代码甚至都没有运行,但绝对时间增加与{} 的情况大致相同。要找到减速的真正根源,您需要深入挖掘。
  • 我的假设是 split-table dict implementation 中涉及的额外分配与差异有很大关系 - 例如,现在有一个 PyDictKeysObject 通过 PyMem_MALLOC 而不是单独分配通过免费列表 - 但要真正确认这一点,我们必须比较 3.2(拆分表实现之前的最后一个版本)的性能,并可能编译 3.5 的一些变体版本,我们更改或删除部分并查看它们如何影响时间。
  • 另外,你指出的差异don't even exist on 3.4;他们是 3.5 的变化。
【解决方案2】:

正如@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

再说一遍:

$ 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 应该可以更好地扩展。

【讨论】:

    【解决方案3】:

    因为没人关心

    您引用的差异大约为数十或数百纳秒。 C 编译器如何优化寄存器使用的微小差异很容易导致此类更改(就像任何数量的其他 C 级优化差异一样)。反过来,这可能是由许多因素引起的,例如 Python (CPython) 的 C 实现中局部变量的数量和使用发生变化,甚至只是切换 C 编译器。

    事实是,没有人积极优化这些微小的差异,所以没有人能够给你一个具体的答案。 CPython 的设计并不是绝对意义上的快速。它被设计为可扩展。因此,例如,您可以将成百上千的项目放入字典中,它会继续表现良好。但是创建字典的绝对速度并不是 Python 实现者最关心的问题,至少在差异如此之小时是这样。

    【讨论】:

    • 谢谢你的回答,凯文。我知道这些差异很小,但是您知道为什么 Python 3.x 中的时间增加而不保持不变吗?
    • @iulian:没有人试图让它保持不变,所以它上升也就不足为奇了。如果它下跌,同样不足为奇。 2.7 和 3.5 之间发生了很多变化。
    • 差异相当大,几乎可以肯定不是由于局部变量的数量或编译器分配的寄存器。 Dict 是 python 内部的基础,具有大量特殊情况优化。差异可能是因为实现的变化。
    • @pvg:我提到实现中的一个变化:“反过来,这可能是由任何数量的事情引起的,例如数字的变化以及 Python 的 C 实现中局部变量的使用...”这是一个实现变化。
    • @Kevin 我认为这在技术上是正确的(最好的正确方式!)但我认为你的答案的要点歪曲了时差和实施变化的规模。这没有发生,因为他们使用了更多的本地变量。
    猜你喜欢
    • 2017-07-10
    • 2014-06-19
    • 1970-01-01
    • 2019-05-03
    • 1970-01-01
    • 2012-04-06
    • 1970-01-01
    • 2023-03-31
    • 1970-01-01
    相关资源
    最近更新 更多