Подтвердить что ты не робот

Почему определение более быстрого определения в 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, так как по здравому смыслу это должно быть наоборот? Я знаю, что эти различия настолько малы, что в большинстве случаев их можно пренебречь. Это было просто наблюдение, которое заставило меня любопытствовать, почему время увеличилось и не осталось по крайней мере таким же?

4b9b3361

Ответ 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 должен масштабироваться лучше.