Чому раннє повернення відбувається повільніше, ніж ще?


179

Це додаткове запитання до відповіді, яке я дав кілька днів тому . Редагувати: схоже, що ОП цього питання вже використав код, який я йому опублікував, щоб задати те саме питання , але я про це не знав. Вибачення. Надані відповіді різні!

По суті я помітив, що:

>>> 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того, викликана умова чи ні.

Я припускаю, що це стосується різного байт-коду, згенерованого двома, але чи хтось здатний детально підтвердити / пояснити?

EDIT: Здається, не всі здатні відтворити мої таймінги, тому я подумав, що може бути корисним дати деяку інформацію в моїй системі. Я запускаю 64-розрядний Ubuntu 11.10 із встановленим за замовчуванням 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
виникло ідентичне запитання щодо ТА, якого я зараз не можу знайти. Вони перевірили згенерований байт-код, і був один додатковий крок. Помічені відмінності дуже залежали від тестера (машини, SO ..), іноді виявляючи лише дуже-дуже невеликі відмінності.
Хоакін

3
На 3.x, обидва виробляють однаковий байт-код, зберігаючи деякий недоступний код ( LOAD_CONST(None); RETURN_VALUE- але, як зазначено, він ніколи не досягнутий) наприкінці with_else. Я дуже сумніваюся, мертвий код робить функцію швидше. Чи може хтось надати dis2.7?

4
Я не зміг цього відтворити. Функціонування з elseі Falseбуло найповільнішим з усіх (152ns). Другий швидкий виявився Trueбез else(143ns), а два інших були в основному однаковими (137ns і 138ns). Я не використовував параметр за замовчуванням і вимірював його %timeitв iPython.
rplnt

Я не можу відтворити ці таймінги, іноді with_else швидше, іноді це версія без_ельси, схоже, вони для мене досить схожі ...
Седрик Жульєн

1
Додані результати розбирання. Я використовую Ubuntu 11.10, 64-розрядний, фондовий 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
@Chris, жодна довжина рядка не повинна бути значною. Перший раз, коли ви хеште рядок, знадобиться час, пропорційний довжині рядка, але тоді обчислений хеш буде кешований у об'єкті рядка, тому наступні хеші - O (1).
Дункан

1
Ну гаразд, я не знав про кешування, але це має сенс
Кріс Еберл

9
Захоплююче! Чи можу я назвати тебе Шерлок? ;) У будь-якому випадку, я сподіваюся, що я не забуду надати вам додаткові бали з виграшею, як тільки питання стане відповідним.
Voo

4
@mac, не зовсім. Я додам трохи про хеш-дозвіл (збирався втиснути його в коментар, але це цікавіше, ніж я думав).
Дункан

6
@Duncan - Дякую вам за те, що знайшли час для ілюстрації хеш-процесу. Найвища відповідь! :)
mac
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.