Почему определение более быстрого определения в Python 2.7, чем в Python 3.x?
Я столкнулся с (не очень необычной) ситуацией, в которой мне пришлось либо использовать выражение 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
У меня было предположение, что 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
Я пытался погрузиться в исходный код Python 3.5 для dict
, но моего знания языка C недостаточно, чтобы найти ответ сам (или, может быть, я даже не ищу в нужном месте).
Итак, мой вопрос:
Что делает более позднюю версию Python более медленной по сравнению с более старой версией Python на относительно простой задаче, такой как определение dict
, так как по здравому смыслу это должно быть наоборот? Я знаю, что эти различия настолько малы, что в большинстве случаев их можно пренебречь. Это было просто наблюдение, которое заставило меня любопытствовать, почему время увеличилось и не осталось по крайней мере таким же?
Ответы
Ответ 1
Пусть разобрать {}
:
>>> from dis import dis
>>> dis(lambda: {})
1 0 BUILD_MAP 0
3 RETURN_VALUE
Python 2.7 реализация BUILD_MAP
TARGET(BUILD_MAP)
{
x = _PyDict_NewPresized((Py_ssize_t)oparg);
PUSH(x);
if (x != NULL) DISPATCH();
break;
}
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();
}
Немного больше кода.
EDIT:
Python 3.4 реализация идентификатора BUILD_MAP точно так же, как 2.7 (спасибо @user2357112). Я копаю глубже, и похоже, что Python 3 мин. Размера dict - это 8 PyDict_MINSIZE_COMBINED const
PyDict_MINSIZE_COMBINED - это начальный размер для любого нового, нераспределенного dict. 8 позволяет диктофонам не более 5 активных записей; эксперименты предположили, что этого достаточно для большинства диктов (состоящих в основном из обычно маленьких мелодий, созданных для передачи аргументов ключевого слова). Создание этого 8, а не 4 уменьшает количество изменений для большинства словарей без существенного дополнительного использования памяти.
Посмотрите _PyDict_NewPresized в 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 создает пустой dict и Python 3.4 создает 7-элементный dict.
Ответ 2
Потому что никто не заботится
Различия, которые вы цитируете, составляют порядка десятков или сотен наносекунд. Небольшое различие в том, как компилятор C оптимизирует использование регистров, может легко вызвать такие изменения (как и любое количество других различий в оптимизации уровня C). Это, в свою очередь, может быть вызвано любым количеством вещей, такими как изменения количества и использования локальных переменных в реализации C на Python (CPython) или даже просто переключение компиляторов C.
Дело в том, что никто не активно оптимизирует эти небольшие различия, поэтому никто не сможет дать вам конкретный ответ. CPython не предназначен для быстрого в абсолютном смысле. Он предназначен для масштабирования. Так, например, вы можете засунуть сотни или тысячи предметов в словарь, и он будет продолжать работать хорошо. Но абсолютная скорость создания словаря просто не является первоочередной задачей для разработчиков Python, по крайней мере, когда различия незначительны.
Ответ 3
Как уже сказал @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 должен масштабироваться лучше.