eFusion 26 ноября 2020

🐍 PyPy: ускоряем Python с минимальными усилиями

Наступает момент, когда приложение на Python упирается в ограничения по производительности. Чтобы не переписывать его на другом языке, код можно ускорить с помощью PyPy.

Перевод публикуется с сокращениями, автор оригинальной статьи Jahongir Rahmonov.

В зависимости от приложения, Python может быть в 100 раз медленнее, чем некоторые языки более низкого уровня. Вот почему многие компании переписывают свои программы, как только скорость Python становится узким местом для пользователей.

PyPy – это совместимый интерпретатор Python и достойная альтернатива CPython 2.7, 3.6, а вскоре и 3.7. Запустив приложение с его помощью, можно получить заметные улучшения скорости.

Python и PyPy

Спецификация Python используется в ряде реализаций, таких как CPython (написанный на C), Jython (написанный на Java), IronPython (написанный для .NET) и PyPy (написанный на Python).

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

Установка

Ваша ОС из коробки должна предоставлять пакет PyPy. На macOS, например, он инсталлируется с помощью Homebrew:

        brew install pypy3
    

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

        $ tar xf pypy3.6-v7.3.1-osx64.tar.bz2
$ ./pypy3.6-v7.3.1-osx64/bin/pypy3
Python 3.6.9 (?, Jul 19 2020, 21:37:06)
[PyPy 7.3.1 with GCC 4.2.1]
Type "help", "copyright", "credits" or "license" for more information.
    

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

PyPy в действии

Чтобы увидеть PyPy в действии, создайте файл Python с именем script.py и поместите в него следующий код:

        total = 0
for i in range(1, 10000):
    for j in range(1, 10000):
        total += i + j

print(f"The result is {total}")
    

Скрипт из двух вложенных циклов for добавляет числа от 1 до 9999 и выводит результат. Чтобы узнать, сколько времени потребуется для его выполнения, добавьте выделенные строки:

        import time

start_time = time.time()

total = 0
for i in range(1, 10000):
    for j in range(1, 10000):
        total += i + j

print(f"The result is {total}")

end_time = time.time()
print(f"It took {end_time-start_time:.2f} seconds to compute")
    

Теперь код выполняет следующие действия:

  • Строка 3 сохраняет значение текущего времени в переменной start_time;
  • Строки с 5 по 8 выполняют циклы;
  • Строка 10 выводит результат;
  • Строка 12 сохраняет время в end_time;
  • Строка 13 выводит разницу между start_time и end_time, чтобы показать, сколько времени потребовалось для выполнения скрипта.

Попробуйте запустить его с помощью Python. Вот что я получаю на MacBook Pro 2015 года:

        $ python3.6 script.py
The result is 999800010000
It took 20.66 seconds to compute
    

Теперь запустим сценарий с помощью PyPy:

        $ pypy3 script.py
The result is 999800010000
It took 0.22 seconds to compute
    

Этот синтетический тест показывает, что PyPy примерно в 94 раза быстрее Python. Для более серьезной проверки взгляните на PyPy Speed Center, где разработчики запускают ночные тесты с различными исполняемыми параметрами. Нужно помнить, что производительность PyPy напрямую зависит от того, что делает ваш код.

PyPy и его особенности

Исторически сложилось так, что PyPy связан с двумя сущностями:

  • Языковым фреймворком RPython для создания интерпретаторов динамических языков;
  • Реализацией Python с использованием этого фреймворка.

Причина, по которой PyPy известен, как написанный на Python (а не на RPython) интерпретатор, заключается в следующем: RPython использует тот же синтаксис, что и Python. Давайте разберемся, как разрабатывается PyPy:

  • Исходный код написан на RPython;
  • Инструменты RPython (translation toolchain) применяются к коду, делая его более эффективным. Они компилируют код в машинный, поэтому под Mac, Windows и Linux необходимы разные версии;
  • Создается двоичный исполняемый файл – интерпретатор Python, который мы использовали для запуска скрипта.

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

Далее мы изучим функции, делающие PyPy таким эффективным.

Just-In-Time (JIT) компилятор

Прежде, чем перейти к JIT-компиляции, рассмотрим свойства компилируемых и интерпретируемых языков программирования.

Компилируемые ЯП более производительны, но их сложно портировать на различные архитектуры и ОС. Интерпретируемые ЯП лучше портируются, но их производительность намного хуже.

Существуют языки, вроде Python, которые сочетают в себе оба свойства: исходный текст сначала компилируется в промежуточный байт-код, а потом интерпретируется CPython. Это позволяет софту работать стабильнее и сохраняет преимущество портируемости.

Однако производительность по-прежнему далека от компилируемой версии, поскольку та способна выполнять множество невозможных для байт-кода оптимизаций. Здесь появляется JIT-компилятор, объединяющий лучшие части обоих миров. Рассмотрим шаги JIT-компиляции, необходимые для обеспечения производительности:

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

Если вспомнить два вложенных цикла из начала статьи, PyPy обнаружил, что одна и та же операция выполняется несколько раз, скомпилировал ее в машинный код, оптимизировал и поменял реализации местами. Вот почему мы увидели значительное увеличение производительности.

Сборщик мусора

Всякий раз, когда вы создаете любые объекты, под них выделяется память. Если неиспользуемые объекты не чистить, память закончится и произойдет сбой программы.

В C и C++ проблему обычно приходится решать вручную. Другие языки программирования, вроде Python и Java, делают это автоматически. Процесс называется автоматической сборкой мусора – существует несколько методов ее выполнения.

В CPython счетчик ссылок на объект увеличивается всякий раз, когда на него ссылаются и уменьшается при разыменовании. Когда счетчик равен нулю, CPython автоматически вызывает функцию освобождения памяти для объекта, но есть один нюанс. Когда количество ссылок большого дерева объектов становится равным нулю, все связанные объекты освобождаются. Возможна длинная пауза, во время которой программа простаивает. Есть также вариант, при котором подсчет ссылок не сработает. Рассмотрим следующий код:

        class A(object):
    pass

a = A()
a.some_property = a
del a
    

В приведенном коде определяется новый класс, создается экземпляр, его свойству присваивается ссылка на себя, а экземпляр удаляется.

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

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

PyPy использует только второй метод. Он периодически ходит по «живым» объектам, начиная с корня. Это дает ему преимущество перед CPython, делая меньше затраченное на управление памятью время. Вместо того, чтобы делать все за один подход, PyPy разбивает работу на части. Такой подход добавляет всего несколько миллисекунд после каждой коллекции, а не сотни, как в CPython.

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

Ограничения PyPy

PyPy не всегда оказывается подходящим инструментом и может заставить приложение работать намного медленнее, чем CPython. Вот почему важно помнить о следующих ограничениях.

Некорректная работа с C-Extensions

Всякий раз, когда вы используете С-расширения, программа будет работать намного медленнее чем в CPython, т. к. они не поддерживаются полностью, и PyPy не может их оптимизировать. Также PyPy должен эмулировать подсчет ссылок для этой части кода, что делает его еще более медленным.

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

Разработчики трудятся над C-Extensions: некоторые пакеты уже портированы на PyPy и работают так же быстро.

Работает только с Long-Running программами

Когда вы запускаете скрипт с помощью PyPy, он совершает много операций, чтобы код работал быстрее. Если скрипт слишком мал, из-за накладных расходов он будет работать медленнее, чем в CPython. С другой стороны, если код большой, эти накладные расходы могут увеличить производительность.

Чтобы в этом убедиться, выполните следующий небольшой скрипт в CPython и PyPy:

        import time

start_time = time.time()

for i in range(100):
    print(i)

end_time = time.time()
print(f"It took {end_time-start_time:.10f} seconds to compute")
    

Есть небольшая задержка, когда вы запускаете его с помощью PyPy, в то время как в CPython старт происходит мгновенно. На MacBook Pro использование CPython займет 0.0004873276 секунды, а в случае с PyPy – 0.0019447803 секунды.

Он не делает компиляцию заранее

PyPy не является полностью компилируемой реализацией Python. Из-за присущего Python динамизма, код невозможно скомпилировать в двоичный файл и повторно его использовать.

PyPy – это рантайм-интерпретатор, который работает быстрее, чем полностью интерпретируемый язык, но медленнее, чем полностью компилируемый.

Заключение

PyPy – это быстрая и эффективная альтернатива CPython. Запустив свой скрипт с его помощью, вы можете получить значительное улучшение скорости, не внося ни одного изменения в код. У него есть ограничения, и вам нужно будет протестировать программу, чтобы проанализировать целесообразность использования альтернативного интерпретатора.

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

Источники

Комментарии 0

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

BUG