为什么 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 元素字典。
我遇到过(不是很不寻常的)情况,在这种情况下我不得不使用 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 元素字典。