Почему раннее возвращение медленнее, чем раньше?

180

Это дополнительный вопрос к ответу, который я дал несколько дней назад . Изменить: кажется, что ОП этого вопроса уже использовал код, который я отправил ему, чтобы задать тот же вопрос , но я не знал об этом. Извиняюсь. Ответы предоставлены разные, хотя!

В основном я заметил, что:

>>> def without_else(param=False):
...     if param:
...         return 1
...     return 0
>>> def with_else(param=False):
...     if param:
...         return 1
...     else:
...         return 0
>>> from timeit import Timer as T
>>> T(lambda : without_else()).repeat()
[0.3011460304260254, 0.2866089344024658, 0.2871549129486084]
>>> T(lambda : with_else()).repeat()
[0.27536892890930176, 0.2693932056427002, 0.27011704444885254]
>>> T(lambda : without_else(True)).repeat()
[0.3383951187133789, 0.32756996154785156, 0.3279120922088623]
>>> T(lambda : with_else(True)).repeat()
[0.3305950164794922, 0.32186388969421387, 0.3209099769592285]

... или другими словами: наличие elseпредложения быстрее независимо от ifтого, вызывается условие или нет.

Я предполагаю, что это связано с различным байт-кодом, сгенерированным этими двумя, но кто-нибудь может подтвердить / объяснить подробно?

РЕДАКТИРОВАТЬ: Кажется, не все могут воспроизвести мои сроки, поэтому я подумал, что было бы полезно дать некоторую информацию о моей системе. Я использую Ubuntu 11.10 64 bit с установленным Python по умолчанию. pythonгенерирует следующую информацию о версии:

Python 2.7.2+ (default, Oct  4 2011, 20:06:09) 
[GCC 4.6.1] on linux2

Вот результаты разборки в Python 2.7:

>>> dis.dis(without_else)
  2           0 LOAD_FAST                0 (param)
              3 POP_JUMP_IF_FALSE       10

  3           6 LOAD_CONST               1 (1)
              9 RETURN_VALUE        

  4     >>   10 LOAD_CONST               2 (0)
             13 RETURN_VALUE        
>>> dis.dis(with_else)
  2           0 LOAD_FAST                0 (param)
              3 POP_JUMP_IF_FALSE       10

  3           6 LOAD_CONST               1 (1)
              9 RETURN_VALUE        

  5     >>   10 LOAD_CONST               2 (0)
             13 RETURN_VALUE        
             14 LOAD_CONST               0 (None)
             17 RETURN_VALUE        
макинтош
источник
1
был такой же вопрос по ТАК не могу найти сейчас. Они проверили сгенерированный байт-код, и был еще один шаг. Наблюдаемые различия были очень зависимы от тестера (машина, ТАК ...), иногда находя только очень и очень маленькие различия.
Хоакин
3
В 3.x оба выпускают идентичный байт-код, сохраняя для некоторого недоступного кода ( LOAD_CONST(None); RETURN_VALUEно, как уже говорилось, он никогда не достигается) в конце with_else. Я очень сомневаюсь, что мертвый код делает функцию быстрее. Может ли кто-нибудь предоставить disна 2.7?
4
Я не смог воспроизвести это. Функция с elseи Falseбыла самой медленной из всех (152 нс). Второй самый быстрый был Trueбез else(143 нс), а два других были в основном одинаковыми (137 нс и 138 нс). Я не использовал параметр по умолчанию и измерял его %timeitв iPython.
rplnt
Я не могу воспроизвести эти тайминги, иногда with_else быстрее, иногда это версия без_открытия, похоже, они очень похожи на меня ...
Седрик Жюльен
1
Добавлены результаты разборки. Я использую Ubuntu 11.10, 64-bit, стандартный Python 2.7 - такая же конфигурация, как у @mac. Я также согласен, что with_elseэто заметно быстрее.
Крис Морган

Ответы:

387

Это чистое предположение, и я не нашел простой способ проверить, правильно ли это, но у меня есть для вас теория.

Я пробовал ваш код и получал те же результаты, without_else()что несколько раз медленнее, чем with_else():

>>> T(lambda : without_else()).repeat()
[0.42015745017874906, 0.3188967452567226, 0.31984281521812363]
>>> T(lambda : with_else()).repeat()
[0.36009842032996175, 0.28962249392031936, 0.2927151355828528]
>>> T(lambda : without_else(True)).repeat()
[0.31709728471076915, 0.3172671387005721, 0.3285821242644147]
>>> T(lambda : with_else(True)).repeat()
[0.30939889008243426, 0.3035132258429485, 0.3046679117038593]

Учитывая, что байт-код идентичен, единственным отличием является имя функции. В частности, проверка синхронизации выполняет поиск глобального имени. Попробуйте переименовать, without_else()и разница исчезнет:

>>> def no_else(param=False):
    if param:
        return 1
    return 0

>>> T(lambda : no_else()).repeat()
[0.3359846013948413, 0.29025818923918223, 0.2921801513879245]
>>> T(lambda : no_else(True)).repeat()
[0.3810395594970828, 0.2969634408842694, 0.2960104566362247]

Я предполагаю, что у without_elseнего есть коллизия хешей с чем-то другим, globals()поэтому поиск по глобальному имени немного медленнее.

Редактировать : словарь с 7 или 8 ключами, вероятно, имеет 32 слота, поэтому на этом основании without_elseимеет коллизию хеша с __builtins__:

>>> [(k, hash(k) % 32) for k in globals().keys() ]
[('__builtins__', 8), ('with_else', 9), ('__package__', 15), ('without_else', 8), ('T', 21), ('__name__', 25), ('no_else', 28), ('__doc__', 29)]

Чтобы уточнить, как работает хеширование:

__builtins__ хеширование до -1196389688, которое уменьшило по модулю размер таблицы (32), означает, что оно хранится в слоте # 8 таблицы.

without_elseхеширует до 505688136, что по модулю 32 уменьшено до 8, поэтому происходит коллизия. Чтобы решить этот Python рассчитывает:

Начиная с:

j = hash % 32
perturb = hash

Повторяйте это, пока мы не найдем свободный слот:

j = (5*j) + 1 + perturb;
perturb >>= 5;
use j % 2**i as the next table index;

что дает 17 для использования в качестве следующего индекса. К счастью, это бесплатно, поэтому цикл повторяется только один раз. Размер хеш-таблицы равен степени 2, так же 2**iкак и размер хеш-таблицы, iэто количество битов, используемых из хеш-значения j.

Каждый зонд в таблице может найти один из них:

  • Слот пуст, в этом случае зондирование прекращается, и мы знаем, что значение отсутствует в таблице.

  • Слот не используется, но использовался в прошлом, и в этом случае мы пробуем следующее значение, рассчитанное, как указано выше.

  • Слот заполнен, но полное значение хеша, хранящееся в таблице, не совпадает с хешем ключа, который мы ищем (это то, что происходит в случае __builtins__vs without_else).

  • Слот заполнен и имеет именно то значение хеша, которое нам нужно, затем Python проверяет, являются ли ключ и объект, который мы ищем, одним и тем же объектом (который в этом случае будет, потому что короткие строки, которые могут быть идентификаторами, интернированы так, идентичные идентификаторы используют одну и ту же строку).

  • Наконец, когда слот заполнен, хеш точно совпадает, но ключи не являются идентичным объектом, тогда и только тогда Python попытается сравнить их на равенство. Это сравнительно медленно, но в случае поиска имен на самом деле не должно происходить.

Дункан
источник
9
@ Крис, нет, длина строки не должна быть значительной. В первый раз, когда вы хэшируете строку, это займет время, пропорциональное длине строки, но затем вычисленный хеш кэшируется в строковом объекте, поэтому последующие хэши - O (1).
Дункан
1
Ах, хорошо, я не знал о кешировании, но это имеет смысл
Крис Эберле
9
Захватывающий! Могу я звать тебя Шерлок? ;) В любом случае, я надеюсь, что не забуду дать вам дополнительные баллы с наградой, как только вопрос станет приемлемым.
Во
4
@ Мак, не совсем. Я добавлю немного о разрешении хеша (собирался втиснуть его в комментарий, но это более интересно, чем я думал).
Дункан
6
@Duncan - Большое спасибо, что нашли время, чтобы проиллюстрировать процесс хеширования. Лучший ответ! :)
Mac