Информатика

Codon: компилятор Python

Компиляция python в машинный код с помощью компилятора LLVM.

У Python в течение многих лет не было хорошего компилятора, который компилировал бы эффективный машинный код. Python сам по себе не самый быстрый язык, а нативный код C превосходит его во много раз. Для систем реального времени, игр, симуляций, обработки сигналов и приложений для научных вычислений без таких библиотек, как numpy, базовый python слишком медленный. Были и другие компиляторы, которые компилируют в машинный код, но они не создают исполняемые файлы и часто требуют некоторой интерпретации во время выполнения. Есть PyPy, который компилируется JIT, но он использует только ограниченный набор python и по-прежнему требует интерпретации во время выполнения от байт-кода до машинного кода, специфичного для процессора.

Однако недавно появился новый компилятор Python, который мне кажется интересным. Codon от Exaloop — это новый компилятор Python, который может напрямую компилироваться в машинный код. Он использует структуру LLVM для компиляции в байт-код LLVM, а затем в конкретный машинный код. Я заинтригован, потому что с этими дополнениями Python становится полезным для многих приложений, которых раньше не было. Игры потому, что это возможно, потому что они требуют множества матричных вычислений в режиме реального времени, которые такие библиотеки, как Numpy, делают невозможными. Научные вычисления, хотя с векторизованными операциями в numpy выполнимы, сильно ограничивают возможности языка, которые можно использовать; обычно нельзя использовать собственные циклы Python.

Есть системы реального времени, которые меня интересуют, такие как обработка сигналов и звука, которые требуют быстрых вычислений, которые Python не может сделать легко, и часто требуют расширений C/C++. Другие приложения, такие как машинное обучение, часто требуют собственных расширений в библиотеках, таких как Tensorflow, но теперь из-за компиляторов, таких как Codon, вероятно, можно обойтись без них.

Бенчмаркинг

Для сравнения я сначала напишу декоратор в файле с именем Timing.py. Этот декоратор принимает функцию и возвращает обернутую функцию, которая умножает время, в течение которого входная функция выполняется в среднем в наносекундах после определенного количества попыток.

import time


def perf_time(n=4):
    """Decorator which times function on average

    Args:
        n (int, optional): Number of times to run function. Defaults to 4.
    """

    def decorator(fn):
        def wrapper(*args, **kwargs):
            times = []
            for _ in range(n):
                start = time.perf_counter_ns()
                fn(*args, **kwargs)
                end = time.perf_counter_ns()
                dur = int(end - start)
                times.append(dur)
            avg = sum(times) / n
            print(f"Function took on average {avg}ns to run after {n} trials.")

        return wrapper

    return decorator

Затем я впервые использую простой факторный алгоритм в файле fac.py, используя цикл для вычисления 10 000!

from timing import perf_time


@perf_time(n=10)
def factorial(n):
    p = 1
    for k in range(2, n + 1):
        p *= k
    return p


factorial(10_000)

Мы запустим алгоритм, используя дефолтный интерпретатор CPython версии 3.10.

python fac.py

Функция выполнялась в среднем за 28766165,7 нс после 10 испытаний.

Теперь давайте попробуем скомпилировать с помощью Codon. Чтобы скомпилировать этот файл, мы запускаем команду.

codon build -release -exe fac.py

Опция -release компилирует код со всеми включенными оптимизациями, а опция -exe компилирует его в исполняемый файл.

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

codon run -release fac.py

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

./fac

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

Функция выполнялась в среднем за 69,8 нс после 10 испытаний.

Версия CPython заняла почти на 41 212 200% больше времени, чем версия Codon. Святое дерьмо, это впечатляет, действительно впечатляет.

Давайте попробуем другой алгоритм, сортировку выбором, которая находит минимальный элемент после каждого индекса и меняет его местами с текущим элементом. Мы отсортируем список из 1000 случайных чисел, которые будут перетасованы.

import random
from timing import perf_time


@perf_time(n=10)
def selection_sort(l):
    for i in range(len(l) - 1):
        min_idx = i
        for j in range(i + 1, len(l)):
            if l[j] < l[min_idx]:
                min_idx = j
        l[i], l[min_idx] = l[min_idx], l[i]


nums = [random.randint(0, 1000) for _ in range(1000)]
random.shuffle(nums)
selection_sort(nums)

После запуска получаем время версии CPython.

Функция выполнялась в среднем 5 102 0752,8 нс после 10 испытаний.

Принимая во внимание, что версия Codon занимает гораздо меньше времени.

Функция выполнялась в среднем за 1,01499e+06 нс после 10 испытаний.

Версия CPython заняла на 4927% больше времени, чем версия Codon. Более скромный выигрыш, но все же достаточный, чтобы оказать влияние.

графический процессор

Еще одной интересной особенностью Codon является его способность писать ядра для графического процессора. Эти программы работают на графическом процессоре, таком как шейдер, и могут выполнять параллельные вычисления на тысячах различных процессоров. В качестве примера из файла readme ядро ​​для вычисления множества Мандельброта будет выглядеть так.

import gpu

MAX    = 1000  # maximum Mandelbrot iterations
N      = 4096  # width and height of image
pixels = [0 for _ in range(N * N)]

def scale(x, a, b):
    return a + (x/N)*(b - a)

@gpu.kernel
def mandelbrot(pixels):
    idx = (gpu.block.x * gpu.block.dim.x) + gpu.thread.x
    i, j = divmod(idx, N)
    c = complex(scale(j, -2.00, 0.47), scale(i, -1.12, 1.12))
    z = 0j
    iteration = 0

    while abs(z) <= 2 and iteration < MAX:
        z = z**2 + c
        iteration += 1

    pixels[idx] = int(255 * iteration/MAX)

mandelbrot(pixels, grid=(N*N)//1024, block=1024)

С помощью декоратора gpu.kernel функцию можно превратить в программу GPU, которую можно запускать параллельно. Это значительно упрощает матричные, графические и математические операции без использования такой библиотеки, как numpy.

Недостатки

Есть несколько недостатков; до сих пор не были портированы общие библиотеки, такие как numpy, scikit-learn, scipy и даже игровые библиотеки, такие как pygame. Можно было бы заставить их работать, но пока мне это не удалось. Я также не мог использовать такие модули, как набор текста или functools с Codon. Я не мог использовать обертки, например, из functools, чтобы предоставить декоратору контекстную информацию. На данный момент Codon, похоже, лучше всего работает с современным ванильным питоном без дополнительных модулей или расширений. Если Codon будет включен в Python Foundation, возможно, будет проще интегрировать его с новыми функциями Python; однако на данный момент он полагается на обновления исключительно от Exaloop.

Стоит ли использовать Кодон? Возможно, если вам нужна более высокая производительность, а ваш код не требует много размышлений, типизированного Python или большинства библиотек. Если вы используете обширные библиотеки, это, вероятно, не лучший вариант, пока Codon не поддерживает эти библиотеки. Codon полезен, но пока он остается простым компилятором.