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