Является ли "x <y <z" быстрее, чем "x <y и y <z"?

От эта страница, мы знаем, что:

Связанные сравнения выполняются быстрее, чем при использовании оператора 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 ГГц.

Ответ 1

Разница в том, что в 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

Ответ 2

Оптимальный байт-код для обеих функций, которые вы определили, будет

          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 (верхняя часть стека справа, -- делит до и после, trailing ? указывает то, что может или не может быть там). Обратите внимание, что 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 попробовать второй - это обеспечит гораздо большее улучшение для тех же усилий, которые вы можете применить.

Ответ 3

Поскольку разница в выходе, по-видимому, связана с отсутствием оптимизации, я думаю, вы должны игнорировать эту разницу для большинства случаев - может быть, разница будет уходить. Разница заключается в том, что только y следует оценивать только один раз, и это решается путем дублирования его в стеке, который требует дополнительного POP_TOP - возможно, решение использовать LOAD_FAST возможно.

Важное различие заключается в том, что в x<y and y<z второй y следует оценивать дважды, если x<y оценивается как true, это имеет значение, если оценка y занимает значительное время или имеет побочные эффекты.

В большинстве сценариев вы должны использовать x<y<z, несмотря на то, что он несколько медленнее.

Ответ 4

Прежде всего, ваше сравнение практически бессмысленно, потому что две различные конструкции были не введены для обеспечения улучшения производительности, поэтому вам не следует решать, использовать ли это вместо другого на основе на этом.

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

  • Яснее и точнее по своему значению.
  • Его семантика - это то, что вы ожидаете от "математического значения" сравнения: evalute x, y и z один раз и проверьте, выполнено ли все условие. Использование and изменяет семантику, оценивая y несколько раз, что может изменить результат.

Поэтому выберите один вместо другого в зависимости от семантики, которую вы хотите, и , если они эквивалентны, является ли один из них более читаемым, чем другой.

Это говорит о том, что более разобранный код не подразумевает более медленный код. Однако выполнение дополнительных операций байт-кода означает, что каждая операция проще, и все же она требует итерации основного цикла. Это означает, что , если выполняемые вами операции чрезвычайно быстрые (например, поиск локальной переменной, как вы там делаете), тогда накладные расходы на выполнение дополнительных операций байт-кода могут иметь значение.

Но обратите внимание, что этот результат не содержит не в более общей ситуации, только для "худшего случая", с которым вы сталкиваетесь с профилем. Как отмечали другие, если вы измените y на то, что занимает немного больше времени, вы увидите, что результаты меняются, потому что привязанные нотации оценивают его только один раз.

Резюмируя:

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