Ускоряем Python — 4 быстрых компилирующих транслятора для Python

Эта статья является переводом и адаптацией оригинальной англоязычной статьи авторства Дэвида Болтона.

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

Однако, по мере того, как язык набирает популярность, разработчики хотят создавать и быстро работающие программы на Python, поэтому за последние годы появилось несколько компиляторов Python, включая IronPython и Jython.

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

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

Я хотел сравнить несколько компиляторов Python на одной платформе, особенно те, которые поддерживают Python 3.x. В итоге я выбрал четыре, все они работают на Ubuntu Linux: Nuitka, PyPy, Cython и cx_Freeze.

Сравнение трансляторов Python

В качестве бенчмарка будет использоваться пакет PyStone, адаптация C-программы, которую сделал Гвидо ван Россум, создатель Python (сама C-программа была переводом Ада-программы). Я нашел версию бенчмарка от Кристофера Арндта, которая способна тестировать Python 3. Чтобы получить представление о базовой производительности, оценим производительность CPython (то есть стандартного Python) с PyStone.

Все бенчмарки при переводе были выполнены заново на CPU Intel(R) Core(TM) i5-7440HQ CPU @ 2.80GHz

$ python2 pystone.py 1000000
Pystone(1.1.1) time for 1000000 passes = 3.61152
This machine benchmarks at 276892 pystones/second

$ python3 pystone.py 1000000
Pystone(1.1.1) time for 1000000 passes = 4.07254
This machine benchmarks at 245547 pystones/second

Как видите, между производительностью теста в Python 2 и 3 есть существенная разница (чем больше Pystones в секунду, тем лучше). В следующих разбивках все компиляторы используют Python 3.

Nuitka

В Ubuntu 18.04 установить Nuitka возможно с помощью APT:

$ sudo apt update
$ sudo apt install nuitka clang

Результат выполнения бенчмарка для компилятора Nuitka:

# Сборка для GCC
$ nuitka pystone.py
$ ./pystone.exe 1000000
Pystone(1.1.1) time for 1000000 passes = 2.67537
This machine benchmarks at 373780 pystones/second

# Сборка для Clang
$ nuitka pystone.py --clang
$ ./pystone.exe 1000000
Pystone(1.1.1) time for 1000000 passes = 2.64646
This machine benchmarks at 377863 pystones/second

# Использование GCC с оптимизацией 
$ nuitka pystone.py --lto
$ ./pystone.exe 1000000
Pystone(1.1.1) time for 1000000 passes = 2.6195
This machine benchmarks at 381753 pystones/second

Как можно видеть, Nuitka позволила получить увеличение производительности на 50%, по сравнению со стандартной реализацией Python 3.

PyPy

Гидо ван Россум однажды сказал: «Если вы хотите, чтобы ваш код работал быстрее, вам, вероятно, следует просто использовать PyPy». Я загрузил переносимые двоичные файлы в папку, а в папке bin скопировал pystone.py. Затем я запустил это так: Мы просто установили PyPy 3 с помощью Ubuntu Snap:

$ sudo snap install pypy3 --classic

Результаты бенчмарка:

$ pypy3 pystone.py 1000000
Pystone(1.1.1) time for 1000000 passes = 0.359845
This machine benchmarks at 2.77897e+06 pystones/second

$ pypy3 pystone.py 1000000
Pystone(1.1.1) time for 1000000 passes = 0.26776
This machine benchmarks at 3.73469e+06 pystones/second

$ pypy3 pystone.py 1000000
Pystone(1.1.1) time for 1000000 passes = 0.147859
This machine benchmarks at 6.7632e+06 pystones/second

Для данного теста результаты выполнения до «разгона» показывают более 10 кратное, а после «разгона» более чем 26 кратное ускорение производительности.

Создание исполняемого файла требует больше работы. Вы должны написать свой Python в подмножестве RPython.

Cython

Cython — это не просто компилятор для Python; это языковое надмножество языка Python, который поддерживает взаимодействие с C/C ++. CPython написан на C, поэтому это язык, который обычно хорошо сочетается с Python:

$ sudo apt install cython3 pkg-config

Сборка программы с помощью Cython немного сложна. Это не похоже на Nuitka, которая просто работает из коробки:

$ cython3 pystone.py --embed
$ gcc $(python3-config --includes) pystone.c -lpython3.6m -o pystone.exe


$ ./pystone.exe 1000000
Pystone(1.1.1) time for 1000000 passes = 4.8549
This machine benchmarks at 205978 pystones/second

Производительность оказалась весьма низкой, гораздо ниже, чем у стандартного CPython. Однако, Cython требует, чтобы вы проделали дополнительную работу, указав типы переменных. Python — это динамический язык, поэтому типы не указываются; Cython использует статическую компиляцию, а использование переменных с типом Си позволяет создавать гораздо более оптимизированный код — документация довольно обширна и требует глубокого изучения.

Cx_Freeze

Cx_freeze — это набор скриптов и модулей для «замораживания» скриптов Python в исполняемые файлы. Установить cx_Freeze можно с помощью PIP3:

sudo pip3 install cx_Freeze --upgrade
$ cxfreeze pystone.py -O -s --target-dir dist
Missing modules:
? __main__ imported from bdb, pdb
? _dummy_threading imported from dummy_threading
? _frozen_importlib imported from importlib, importlib.abc
? _frozen_importlib_external imported from importlib, importlib._bootstrap, importlib.abc
? _winapi imported from subprocess
? _winreg imported from platform
? java.lang imported from platform
? msvcrt imported from subprocess
? nt imported from ntpath, os, shutil
? org.python.core imported from copy, pickle
? os.path imported from os, pkgutil, py_compile, tracemalloc, unittest, unittest.util
? vms_lib imported from platform
? winreg imported from mimetypes, platform
This is not necessarily a problem - the modules may not be needed on this platform.

Copying data from package collections...
Copying data from package email...
Copying data from package encodings...
Copying data from package html...
Copying data from package http...
Copying data from package importlib...
Copying data from package logging...
Copying data from package pydoc_data...
Copying data from package unittest...
Copying data from package urllib...
Copying data from package xml...

$ dist/pystone 1000000
Pystone(1.1.1) time for 1000000 passes = 5.35072
This machine benchmarks at 186891 pystones/second

Как можно видеть, производительность даже ниже, чем у стандартного интерпретатора CPython. Данное решение разумно использовать только для упаковки всего Python-окружения в независимый исполняемый пакет. Стоит отметить, что для этой цели можно использовать и Pyinstaller.

Заключение

Я в восторге от производительности PyPy. Компиляция была очень быстрой, и приложение работало в десятки раз быстрее аналогов и оригинального кода CPython. Если вы хотите распространять бинарный файл, выбирайте Nuitka — решение дает как ускорение, так и позволяет выполнить упаковку кода.

От переводчика: в статье рассмотрено использование решений PyPy, Nuitka, Cython и Cx_Freeze для очень простого кода, который без проблем собирается данными трансляторами. Код, используемый в реальных приложениях, может быть затруднительно скомпилировать или его производительность может стать еще хуже, чем у стандартного Python. Необходимо производить бенчмарки на том коде, который вы собираетесь распространять, поскольку синтетические бенчмарки, как в этой статье не дают представления о реальном варианте использования, который будет в вашем случае.