Чи "x <y <z" швидше, ніж "x <y і y <z"?


129

З цієї сторінки ми знаємо, що:

Схематичні порівняння швидше, ніж використання andоператора. Пишіть x < y < zзамість x < y and y < z.

Однак я отримав інший результат тестування таких фрагментів коду:

$ python -m timeit "x = 1.2" "y = 1.3" "z = 1.8" "x < y < z"
1000000 loops, best of 3: 0.322 usec per loop
$ python -m timeit "x = 1.2" "y = 1.3" "z = 1.8" "x < y and y < z"
1000000 loops, best of 3: 0.22 usec per loop
$ python -m timeit "x = 1.2" "y = 1.3" "z = 1.1" "x < y < z"
1000000 loops, best of 3: 0.279 usec per loop
$ python -m timeit "x = 1.2" "y = 1.3" "z = 1.1" "x < y and y < z"
1000000 loops, best of 3: 0.215 usec per loop

Здається, x < y and y < zце швидше, ніж x < y < z. Чому?

Після пошуку деяких публікацій на цьому веб-сайті (як-от цього ) я знаю, що ключовим моментом є "лише один раз" x < y < z, однак я все ще плутаюсь. Для подальшого вивчення я розібрав ці дві функції за допомогою dis.dis:

import dis

def chained_compare():
        x = 1.2
        y = 1.3
        z = 1.1
        x < y < z

def and_compare():
        x = 1.2
        y = 1.3
        z = 1.1
        x < y and y < z

dis.dis(chained_compare)
dis.dis(and_compare)

А вихід:

## chained_compare ##

  4           0 LOAD_CONST               1 (1.2)
              3 STORE_FAST               0 (x)

  5           6 LOAD_CONST               2 (1.3)
              9 STORE_FAST               1 (y)

  6          12 LOAD_CONST               3 (1.1)
             15 STORE_FAST               2 (z)

  7          18 LOAD_FAST                0 (x)
             21 LOAD_FAST                1 (y)
             24 DUP_TOP
             25 ROT_THREE
             26 COMPARE_OP               0 (<)
             29 JUMP_IF_FALSE_OR_POP    41
             32 LOAD_FAST                2 (z)
             35 COMPARE_OP               0 (<)
             38 JUMP_FORWARD             2 (to 43)
        >>   41 ROT_TWO
             42 POP_TOP
        >>   43 POP_TOP
             44 LOAD_CONST               0 (None)
             47 RETURN_VALUE

## and_compare ##

 10           0 LOAD_CONST               1 (1.2)
              3 STORE_FAST               0 (x)

 11           6 LOAD_CONST               2 (1.3)
              9 STORE_FAST               1 (y)

 12          12 LOAD_CONST               3 (1.1)
             15 STORE_FAST               2 (z)

 13          18 LOAD_FAST                0 (x)
             21 LOAD_FAST                1 (y)
             24 COMPARE_OP               0 (<)
             27 JUMP_IF_FALSE_OR_POP    39
             30 LOAD_FAST                1 (y)
             33 LOAD_FAST                2 (z)
             36 COMPARE_OP               0 (<)
        >>   39 POP_TOP
             40 LOAD_CONST               0 (None)

Здається, що команди x < y and y < zмають менше розібраних команд, ніж x < y < z. Чи варто розглянути x < y and y < zшвидше, ніж x < y < z?

Тестовано на Python 2.7.6 на процесорі Intel (R) Xeon (R) E5640 при 2,67 ГГц.


8
Більше розібраних команд не означає більше складності та повільнішого коду. Однак побачивши ваші timeitтести, я зацікавився цим.
Марко Бонеллі

6
Я припускав, що різниця швидкостей від "оцінюється один раз" настає, коли yце не просто пошук змінної, а більш дорогий процес, як виклик функції? Тобто 10 < max(range(100)) < 15швидше, ніж 10 < max(range(100)) and max(range(100)) < 15тому max(range(100)), що викликається один раз для обох порівнянь.
zehnpaard

2
@MarcoBonelli Це робиться, коли розібраний код 1) не містить циклів і 2) кожен окремий байт-код стає дуже швидким, оскільки в цей момент накладні витрати основного циклу набувають значного значення.
Бакуріу

2
Прогнозування галузей може зіпсувати ваші тести.
Corey Ogburn

2
@zehnpaard Я з вами згоден. Коли "y" - це більше, ніж просте значення (наприклад, виклик функції чи обчислення), я очікував би, що "y" оцінюється один раз у x <y <z, щоб мати набагато помітніший вплив. У наведеному випадку ми знаходимось у рядках помилок: переважають ефекти (невдало) прогнозування гілок, менш оптимального байт-коду та інших ефектів, що стосуються платформи / процесора. Це не скасовує загальне правило про те, що оцінювання одного разу краще (а також легше для читання), але показує, що це може не додати стільки значення в крайніх межах.
MartyMacGyver

Відповіді:


111

Різниця в тому, що в x < y < z yоцінюється лише один раз. Це не має великої різниці, якщо y - змінна, але це робиться, коли це виклик функції, який вимагає певного часу для обчислення.

from time import sleep
def y():
    sleep(.2)
    return 1.3
%timeit 1.2 < y() < 1.8
10 loops, best of 3: 203 ms per loop
%timeit 1.2 < y() and y() < 1.8
1 loops, best of 3: 405 ms per loop

18
Звичайно, тут може бути і смислова різниця. Мало того, що y () може повернути два різних значення, але за допомогою змінної оцінка менш операційного в x <y може змінити значення y. Ось чому його часто не оптимізують у байтовому коді (якщо, наприклад, немісцева змінна або бере участь у закритті)
Random832

Просто цікаво, навіщо вам потрібна sleep()функція всередині?
Проф

@Prof Це для імітації функції, яка потребує певного часу для обчислення результату. Якщо функції повернуться одразу, різниці між двома результатами часу не буде багато.
Роб

@Rob Чому б не було великої різниці? Це було б 3 мс до 205 мс, що демонструє це досить добре, чи не так?
Проф

@Prof Вам не вистачає точки, яка y () обчислюється вдвічі, тобто 2х200мс замість 1x200мс. Решта (3/5 мс) - нерелевантний шум при вимірюванні часу.
Роб

22

Оптимальним буде байт-код для обох визначених вами функцій

          0 LOAD_CONST               0 (None)
          3 RETURN_VALUE

тому що результат порівняння не використовується. Давайте зробимо ситуацію цікавішою, повернувши результат порівняння. Давайте також, щоб результат не був відомий під час компіляції.

def interesting_compare(y):
    x = 1.1
    z = 1.3
    return x < y < z  # or: x < y and y < z

Знову ж таки, дві версії порівняння є семантично однаковими, тому оптимальний байт-код однаковий для обох конструкцій. Як найкраще я можу це опрацювати, це виглядатиме так. Я коментував кожен рядок із вмістом стека до та після кожного коду, у форматі Forth (верхній частині стека праворуч, --ділиться до та після, трейлінг ?вказує на те, що може бути, а може і не бути). Зауважте, що RETURN_VALUEвідкидає все, що трапляється, залишається у стеці під поверненим значенням.

          0 LOAD_FAST                0 (y)    ;          -- y
          3 DUP_TOP                           ; y        -- y y
          4 LOAD_CONST               0 (1.1)  ; y y      -- y y 1.1
          7 COMPARE_OP               4 (>)    ; y y 1.1  -- y pred
         10 JUMP_IF_FALSE_OR_POP     19       ; y pred   -- y
         13 LOAD_CONST               1 (1.3)  ; y        -- y 1.3
         16 COMPARE_OP               0 (<)    ; y 1.3    -- pred
     >>  19 RETURN_VALUE                      ; y? pred  --

Якщо реалізація мови, CPython, PyPy, що завгодно, не генерує цей байт-код (або його власну еквівалентну послідовність операцій) для обох варіантів, що демонструє низьку якість цього компілятора байт-коду . Вихід із послідовностей байт-коду, які ви опублікували вище, є вирішеною проблемою (я думаю, що все, що вам потрібно для цього випадку, - це постійне складання , усунення мертвого коду та краще моделювання вмісту стека; звичайне усунення підвыражения також буде дешевим і цінним ), і насправді немає приводу для того, щоб не зробити цього в сучасній мовній реалізації.

Зараз трапляється, що всі поточні реалізації мови мають неякісні компілятори байт-кодів. Але ви повинні ігнорувати це під час кодування! Притворіть, що компілятор байт-кодів хороший, і напишіть найбільш читабельний код. Напевно, все одно буде досить швидко. Якщо це не так, спершу шукайте алгоритмічні вдосконалення та спробуйте вдруге Cython - це забезпечить набагато більше вдосконалення за ті ж зусилля, ніж будь-які зміни рівня вираження, які ви можете застосувати.


Ну, найважливіша з усіх оптимізацій мала б бути можливою в першу чергу: вбудовування. І це далеко не "вирішена проблема" для динамічних мов, які дозволяють динамічно змінювати реалізацію функції (хоча це можливо - HotSpot може робити подібні речі, і такі речі, як Graal, працюють над тим, щоб зробити такі оптимізації доступними для Python та інших динамічних мов ). А оскільки саму функцію можна викликати з різних модулів (або дзвінок може генеруватися динамічно!), Ви дійсно не можете зробити ці оптимізації там.
Voo

1
@Voo Мій байтовий код, оптимізований рукою, має точно таку саму семантику, що й оригінал, навіть за наявності довільної динамічності (один виняток: a <b ≡ b> a передбачається). Крім того, вкладка завищена. Якщо ви робите занадто багато цього - і занадто просто це робити занадто багато - ви забиваєте I-кеш і втрачаєте все, що ви отримали, а потім і деякі.
zwol

Ти маєш рацію. Я думав, що ти мав на увазі, що ти хочеш оптимізувати свій interesting_compareпростий байт-код у верхній частині (який би працював лише з вбудованими лініями). Повністю офтопік, але: Вбудовування - це одна з найважливіших оптимізацій для будь-якого компілятора. Ви можете спробувати запустити деякі орієнтири, скажімо, HotSpot на реальних програмах (а не на деякі математичні тести, які витрачають 99% свого часу в один гарячий цикл, який оптимізовано вручну [і, отже, більше не має функцій дзвінків]), коли ви відключите його можливість вбудувати що-небудь - ви побачите великі регресії.
Voo

@Voo Так, простий байт-код вгорі повинен був бути "оптимальною версією" оригінального коду OP, не так interesting_compare.
zwol

"один виняток: a <b ≡ b> a передбачається" → що просто не відповідає дійсності в Python. Крім того, я думаю, що CPython не може навіть по-справжньому припустити, що операції з yне змінюють стек, оскільки він має багато інструментів налагодження.
Ведрак

8

Оскільки, здається, різниця у виході пов'язана з відсутністю оптимізації, я думаю, ви повинні ігнорувати цю різницю для більшості випадків - можливо, різниця піде. Різниця полягає в тому, що yслід оцінювати лише один раз, і це вирішується шляхом дублювання його на стеці, що вимагає додаткової можливості POP_TOP- рішення для використання LOAD_FASTможливо можливо.

Хоча важлива відмінність полягає в тому, що в x<y and y<zдругому yслід оцінювати двічі, якщо x<yоцінюють як істинне, це має наслідки, якщо оцінка yзаймає багато часу або має побічні ефекти.

У більшості сценаріїв ви повинні використовуватись, x<y<zнезважаючи на те, що це дещо повільніше.


6

Перш за все, ваше порівняння є майже безглуздим, оскільки дві різні конструкції не були введені для покращення продуктивності, тому не слід вирішувати, чи використовувати один замість іншого на основі цього.

x < y < zконструкція:

  1. Ясніший і пряміший у своєму значенні.
  2. Його семантика - це те, чого ви очікували від "математичного значення" порівняння: оцініть x, yа z раз і перевірте, чи виконується вся умова. Використання andзмінює семантику шляхом оцінки yдекількох разів, що може змінити результат .

Тому вибирайте один замість іншого, залежно від потрібної семантики, і, якщо вони еквівалентні, чи є одна читабельніша за іншу.

Це говорило: більше розібраний код не означає більш повільний код. Однак виконання більше операцій з байт-кодом означає, що кожна операція є простішою, але вона вимагає ітерації основного циклу. Це означає, що якщо операції, які ви виконуєте, надзвичайно швидкі (наприклад, локальний пошук змінних, як ви робите там), то накладні витрати на виконання більшої кількості байт-кодів можуть мати значення.

Але зауважте, що цей результат не відповідає більш загальній ситуації, а лише "гіршому випадку", який ви маєте на меті. Як зазначали інші, якщо ви перейдете yна щось, що займе ще трохи часу, ви побачите, що результати змінюються, тому що ланцюгова нотація оцінює її лише один раз.

Підведення підсумків:

  • Розгляньте семантику перед виконанням.
  • Враховуйте читабельність.
  • Не довіряйте мікро-орієнтирам. Завжди профіліруйте різні параметри, щоб побачити, як поводиться функція / термін вираження по відношенню до зазначених параметрів, і подумайте, як ви плануєте його використовувати.

5
Я думаю, що ваша відповідь не містить прямого і релевантного факту, що сторінка, що цитується, в конкретному випадку питання - порівнюючи плавки, просто помилкова. Ланцюгове порівняння не швидше, як видно як в вимірах, так і в генерованому байтовому коді.
пвг

30
Відповідь на тег із тегом "напевно, ви не повинні так багато думати про продуктивність" для мене не здається корисним. Ви робите потенційні покровительські припущення щодо розуміння опитувачем загальних принципів програмування, а потім в основному говорите про них, а не про проблему.
Бен Міллвуд

@Veerdac ви неправильно читаєте коментар. Пропонована оптимізація в оригінальному документі, на яку ОП покладалася, є помилковою, що стосується поплавків як мінімум. Це не швидше.
пвг
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.