Оптимизирует ли Python переменную, которая используется только как возвращаемое значение?

106

Есть ли разница между двумя следующими фрагментами кода? Первый присваивает значение переменной в функции, а затем возвращает эту переменную. Вторая функция просто возвращает значение напрямую.

Превращает ли Python их в эквивалентный байт-код? Один из них быстрее?

Случай 1 :

def func():
    a = 42
    return a

Случай 2 :

def func():
    return 42
Джайеш
источник
5
Если вы используете dis.dis(..)оба, вы увидите, что есть разница , так что да. Но в большинстве реальных приложений накладные расходы по сравнению с задержкой обработки в функции не так велики.
Виллем Ван Онсем
4
Есть две возможности: (а) Вы собираетесь вызывать эту функцию много (то есть как минимум миллион) раз в узком цикле. В этом случае вы вообще не должны вызывать функцию Python, а вместо этого должны векторизовать свой цикл, используя что-то вроде библиотеки numpy. (б) Вы не собираетесь вызывать эту функцию много раз. В этом случае разница в скорости между этими функциями слишком мала, чтобы о ней беспокоиться.
Артур Такка,

Ответы:

138

Нет, это не так .

Компиляция в байтовый код CPython проходит только через небольшой оптимизатор-глазок, который предназначен для выполнения только основных оптимизаций (см. Test_peepholer.py в наборе тестов для получения дополнительной информации об этих оптимизациях).

Чтобы disувидеть, что на самом деле произойдет, используйте * для просмотра созданных инструкций. Для первой функции, содержащей присвоение:

from dis import dis
dis(func)
  2           0 LOAD_CONST               1 (42)
              2 STORE_FAST               0 (a)

  3           4 LOAD_FAST                0 (a)
              6 RETURN_VALUE

А для второй функции:

dis(func2)
  2           0 LOAD_CONST               1 (42)
              2 RETURN_VALUE

В первой используются еще две (быстрые) инструкции: STORE_FASTи LOAD_FAST. Они позволяют быстро сохранить и получить значение в fastlocalsмассиве текущего кадра выполнения. Затем в обоих случаях RETURN_VALUEвыполняется а. Итак, второй работает немного быстрее из-за меньшего количества команд, необходимых для выполнения.

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

В конце концов, не стоит беспокоиться о таких мелких проблемах, как эта. Выигрыш в скорости крошечный, постоянный и превосходит накладные расходы, связанные с интерпретацией Python.

* dis- это небольшой модуль Python, который дизассемблирует ваш код, вы можете использовать его, чтобы увидеть байт-код Python, который будет выполнять виртуальная машина.

Примечание. Как также указано в комментарии @Jorn Vernee, это относится к реализации Python в CPython. Другие реализации могут делать более агрессивные оптимизации, если они того пожелают, а CPython - нет.

Димитрис Фасаракис Хиллиард
источник
11
Не питон (c ++), поэтому я не знаю, как это работает под капотом, но не следует ли оптимизировать первый случай для второго случая? Хороший компилятор C ++ сделает эту оптимизацию.
NathanOliver
7
@NathanOliver, это действительно не так, Python будет делать, как сказано здесь, даже не пытаясь сыграть с умом.
Димитрис Фасаракис Хиллиард
80
Тот факт, что совершенно разумная и разумная догадка @NathanOliver об ответе на этот вопрос, на мой взгляд, является доказательством того, что это не "самоочевидный", "ерунда", "глупый" вопрос, на который можно ответить. «воспользовавшись моментом, чтобы подумать», как пытается заставить нас поверить TigerhawkT3. Это правильный и интересный вопрос, на который я не был уверен, несмотря на то, что много лет был профессиональным программистом на Python.
Марк Эмери
Компилятор Python в лучшем случае «консервативен», а не «очень консервативен». Основная цель разработки не в том, чтобы быть «максимально быстрым ... чтобы вы даже не заметили, что существует фаза компиляции». Это вторично после слов «будь простым». Функция с большими константами, такими как «1 << (2 ** 34)» и «b'x '* (2 ** 32)», требует несколько секунд для компиляции и генерации констант размером в ГБ, даже если функция никогда не запустить. Большая строка даже будет отброшена компилятором. Предлагаемые исправления для этих случаев были отклонены, так как они сделали бы компилятор слишком сложным.
Эндрю
@AndrewDalke спасибо за комментарий инсайдеров по этому поводу, я изменил формулировку, чтобы решить проблемы, на которые вы указали.
Димитрис Фасаракис Хиллиард
3

Оба в основном одинаковы, за исключением того, что в первом случае объект 42просто присваивается переменной с именем aили, другими словами, имена (т.е. a) относятся к значениям (т.е. 42). Технически он не выполняет никаких назначений в том смысле, что никогда не копирует никаких данных.

При returnэтом именованная привязка aвозвращается в первом случае, а объект 42возвращается во втором случае.

Для получения дополнительной информации обратитесь к этой замечательной статье Неда Батчелдера.

kmario23
источник