Почему цепные выражения операторов медленнее, чем их расширенный эквивалент?

В python можно связать операторов таким образом:

a op b op c

Что оценивается

a op b and b op c 

С той лишь разницей, что b оценивается только один раз (так, что-то более похожее на t = eval(b); a op t and t op c).

Это выгодно с точки зрения, что оно очень читаемо и более кратким, чем эквивалентная версия с явной связью (с использованием and).

Однако... Я заметил, что существует незначительная разница в производительности между закодированными выражениями и эквивалентом, будь то три операнда или 20. Это становится очевидным, когда вы выполняете эти операции.

import timeit 

timeit.timeit("a <= b <= c", setup="a,b,c=1,2,3")
0.1086414959972899

timeit.timeit("a <= b and b <= c", setup="a,b,c=1,2,3")
0.09434155100097996

А также,

timeit.timeit("a <= b <= c <= d <= e <= f", setup="a,b,c,d,e,f=1,2,3,4,5,6")
0.2151330839988077

timeit.timeit("a <= b and b <= c and c <= d and d <= e and e <= f", setup="a,b,c,d,e,f=1,2,3,4,5,6")
0.19196406500122976

Примечание. Все тесты проводились с помощью Python-3.4.

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

import dis

dis.dis("a <= b <= c")
  1           0 LOAD_NAME                0 (a)
              3 LOAD_NAME                1 (b)
              6 DUP_TOP
              7 ROT_THREE
              8 COMPARE_OP               1 (<=)
             11 JUMP_IF_FALSE_OR_POP    21
             14 LOAD_NAME                2 (c)
             17 COMPARE_OP               1 (<=)
             20 RETURN_VALUE
        >>   21 ROT_TWO
             22 POP_TOP
             23 RETURN_VALUE 

Сравните это с,

dis.dis("a <= b and b <= c")
  1           0 LOAD_NAME                0 (a)
              3 LOAD_NAME                1 (b)
              6 COMPARE_OP               1 (<=)
              9 JUMP_IF_FALSE_OR_POP    21
             12 LOAD_NAME                1 (b)
             15 LOAD_NAME                2 (c)
             18 COMPARE_OP               1 (<=)
        >>   21 RETURN_VALUE

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

Вот как я это интерпретировал. В первом случае переменные переносятся на какой-то стек и последовательно отображаются для сравнения. Все переменные отображаются только один раз. Во втором случае нет стека, но по крайней мере (N - 2) операндов приходится загружать в память дважды для сравнения. Похоже, что операция пополнения стека более дорогая, чем загрузка (N - 2) переменных дважды для сравнения, учитывая разницу в скорости.

Вкратце, я пытаюсь понять, почему одна операция всегда медленнее, чем другая, с помощью постоянного фактора. Правильно ли моя гипотеза? Или есть что-то еще для внутренних компонентов python, которых я пропускаю?


Дополнительные критерии:

| System | a <= b <= c         | a <= b and b <= c   | a <= b <= ... <= e <= f | a <= b and ... and e <= f | Credit         |
|--------|---------------------|---------------------|-------------------------|---------------------------|----------------|
| 3.4    | 0.1086414959972899  | 0.09434155100097996 | 0.2151330839988077      | 0.19196406500122976       | @cᴏʟᴅsᴘᴇᴇᴅ     |
| 3.6.2  | 0.06788300536572933 | 0.059271858073771   | 0.1505890181288123      | 0.12044331897050142       | @Bailey Parker |
| 2.7.10 | 0.05009198188781738 | 0.04472208023071289 | 0.11113405227661133     | 0.09062719345092773       | @Bailey Parker |

Ответы

Ответ 1

В CPuton на основе стека на основе исполнения байт-кода, сохранение дополнительной ссылки на b для прикованного сравнения не является бесплатным. Это на уровне "серьезно, не беспокойтесь об этом" дешево, но это не буквально бесплатно, и вы сравниваете его со слегка более дешевой операцией по загрузке локальной переменной.

Код операции COMPARE_OP удаляет объекты, которые он сравнивает со стеком, поэтому для сопоставленного сравнения Python должен создать другую ссылку на b (DUP_TOP) и засунуть в два стека два стека (ROT_THREE), чтобы избавиться от него.

В a <= b and b <= c вместо вышеупомянутого перетасования ссылок Python просто копирует другую ссылку на b из массива fastlocals фрейма fastlocals. Это включает в себя меньшее перетаскивание указателя и еще меньшее отклонение от цикла оценки байт-кода, поэтому оно немного дешевле.