Установка
Ваша ОС из коробки должна предоставлять пакет PyPy. На macOS, например, он инсталлируется с помощью Homebrew :
Чтобы увидеть PyPy в действии, создайте файл Python с именем script.py и поместите в него следующий код:
Исторически сложилось так, что 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 seconds to compute")
Есть небольшая задержка, когда вы запускаете его с помощью PyPy, в то время как в CPython старт происходит мгновенно. На MacBook Pro использование CPython займет 0.0004873276 секунды, а в случае с PyPy – 0.0019447803 секунды.
Он не делает компиляцию заранее
PyPy не является полностью компилируемой реализацией Python. Из-за присущего Python динамизма, код невозможно скомпилировать в двоичный файл и повторно его использовать.
PyPy – это рантайм-интерпретатор, который работает быстрее, чем полностью интерпретируемый язык, но медленнее, чем полностью компилируемый.
Заключение
PyPy – это быстрая и эффективная альтернатива CPython. Запустив свой скрипт с его помощью, вы можете получить значительное улучшение скорости, не внося ни одного изменения в код. У него есть ограничения, и вам нужно будет протестировать программу, чтобы проанализировать целесообразность использования альтернативного интерпретатора.