Как работают компиляторы Python: Nuitka, Cython, Numba, PyPy
Компиляция Python: современные подходы и инструменты
Обычный интерпретатор CPython переводит исходный код в байт-код перед выполнением, но для достижения максимальной производительности или распространения приложений в виде одного исполняемого файла применяются компиляторы. Рассмотрим несколько решений, каждое из которых отвечает на конкретные задачи разработчика.
Как скомпилировать Python-скрипт в исполняемый файл без зависимостей?
Для создания самодостаточного бинарника используется Nuitka. Этот AOT-компилятор транслирует Python-код в C++, а затем в машинный код, упаковывая интерпретатор и все зависимости. Пример команды:
nuitka --standalone --onefile myscript.pyPython programming compiler (компилятор python)
В результате появляется исполняемый файл, который можно запускать даже на машинах без Python. Цель: распространение приложений с закрытым исходным кодом и упрощение развёртывания.
--follow-imports для ручного управления включением модулей, или --nofollow-import-to для исключения ненужных.Как ускорить код Python с помощью статической типизации и трансляции в C?
Cython расширяет синтаксис Python декларациями типов (cdef, cpdef) и генерирует C-расширения, которые могут быть скомпилированы в .pyd/.so. Пример файла fib.pyx:
def fib(int n):
cdef int a = 0, b = 1, i
for i in range(n):
a, b = b, a + b
return aредактор языка python (редакторы python)
Компиляция: cythonize -i fib.pyx. Полученный модуль импортируется из Python и работает намного быстрее. Цель: ускорение узких мест (циклы, численные расчёты) без полного переписывания на C.
Как ускорить численные вычисления в Python с помощью JIT-компиляции?
Numba (часть пакета Anaconda) предоставляет декораторы @jit, @vectorize, которые компилируют функции «на лету» в машинный код. Пример:
from numba import jit
@jit(nopython=True)
def sum_squares(n):
s = 0
for i in range(n):
s += i * i
return s
print(sum_squares(10**7))Первый вызов включает компиляцию (может быть медленным), последующие - быстрые. Цель: ускорение циклов и операций с массивами NumPy без ручной типизации.
Как ускорить выполнение Python-кода без изменения исходного текста за счёт JIT-компилятора?
PyPy - это альтернативный интерпретатор со встроенным JIT-компилятором. Он просто запускает тот же самый скрипт: pypy3 myscript.py. Производительность может быть в 4-10 раз выше CPython для чистого Python-кода. Цель: мгновенное ускорение legacy-проектов, где нельзя менять код.
Расширенные примеры компиляции Python
Nuitka: упаковка проекта с зависимостями
Пример: приложение с библиотеками requests и matplotlib. Команда для компиляции с подавлением лишних модулей:
nuitka --standalone --onefile --nofollow-import-to=tkinter,matplotlib.backends._backend_agg myapp.pyРезультат - исполняемый файл размером около 15-20 МБ. Время компиляции может достигать 2 минут. Для проверки запускаем:
$ ./myapp.exe (вывод приложения)
Обратите внимание: при использовании --onefile все данные распаковываются во временную папку при запуске, что может занять дополнительное время.
Cython: работа с памятью и массивами NumPy
Файл matmul.pyx - перемножение матриц с ручным управлением памятью:
import numpy as np
cimport numpy as cnp
cpdef void matmul_cy(cnp.ndarray[cnp.float64_t, ndim=2] A,
cnp.ndarray[cnp.float64_t, ndim=2] B,
cnp.ndarray[cnp.float64_t, ndim=2] C):
cdef int i, j, k, n = A.shape[0]
for i in range(n):
for j in range(n):
C[i,j] = 0.0
for k in range(n):
C[i,j] += A[i,k] * B[k,j]Компиляция и вызов:
cythonize -i matmul.pyx
python -c "import numpy as np; from matmul import matmul_cy; A = np.random.rand(200,200); B = np.random.rand(200,200); C = np.empty((200,200)); matmul_cy(A,B,C)"Сравнение времени (CPython vs Cython) - ускорение в 10-15 раз. Возможная ошибка: несовместимость типов при передаче обычного list вместо NumPy array.
Numba: векторизация и параллельные вычисления
Пример с @vectorize для поэлементной обработки:
from numba import vectorize, float64
import numpy as np
@vectorize([float64(float64, float64)], target='parallel')
def my_func(x, y):
return x**2 + y**2 + 2*x*y
arr1 = np.random.rand(10**6)
arr2 = np.random.rand(10**6)
result = my_func(arr1, arr2)Без Numba такая операция выполнялась бы медленнее из-за накладных расходов на интерпретацию. Результат - массив result. Также можно использовать prange для циклов с распараллеливанием:
from numba import prange, jit
import numpy as np
@jit(nopython=True, parallel=True)
def sum_parallel(arr):
s = 0.0
for i in prange(arr.size):
s += arr[i]
return s
arr = np.random.rand(10**7)
print(sum_parallel(arr))Результат: около 0.2 секунды против 0.7 на одном ядре.
PyPy: сравнение производительности с CPython
Возьмём бенчмарк на вычисление простых чисел:
def sieve(n):
is_prime = [True] * (n + 1)
is_prime[0] = is_prime[1] = False
for i in range(2, int(n**0.5) + 1):
if is_prime[i]:
for j in range(i*i, n+1, i):
is_prime[j] = False
return [i for i, prime in enumerate(is_prime) if prime]
if __name__ == '__main__':
primes = sieve(10**6)
print(len(primes))Запуск:
time python3 primes.py
time pypy3 primes.pyРезультат (примерный): CPython - 0.45 сек, PyPy - 0.12 сек. Ускорение в 3-4 раза. PyPy показывает лучшие результаты для кода с большим количеством циклов и вызовов функций, но хуже для кода, активно использующего C-расширения.