Ответ 1
Это чистая догадка, и я не понял простой способ проверить, правильно ли это, но у меня есть теория для вас.
Я пробовал ваш код и получал то же самое, 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__
vswithout_else
). -
Слот заполнен и имеет точно значение хеша, которое мы хотим, затем Python проверяет, являются ли ключ и объект, который мы просматриваем, тот же объект (который в этом случае будет потому, что короткие строки которые могут быть идентификаторами, интернированы, поэтому идентичные идентификаторы используют точно такая же строка).
-
Наконец, когда слот заполнен, хеш точно соответствует, но клавиши не являются идентичным объектом, тогда и только тогда Python попытается сравнивая их для равенства. Это сравнительно медленно, но в случай поиска имен не должен происходить фактически.