Итак, что такое Cython?

Cython – это язык, который очень-очень похож на Python с добавлением синтаксических изюминок. Для того, чтобы убедиться в этом давайте взглянем на пример функции, реализованной на Python:

def is_prime_python(n):
  for i in range(2, n):
    if n % i == 0:
      return False
  return True
%%time
_ = is_prime_python(6_700_417)

Output:

CPU times: user 577 ms, sys: 4.35 ms, total: 582 ms
Wall time: 584 ms

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

Теперь рассмотрим визави – Cython и реализацию его функции проверки числа:

%%cython
def is_prime_cython(int n):
  cdef int i
  for i in range(2, n):
    if n % i == 0:
      return False
  return True
%%time
_ = is_prime_cython(6_700_417)

Output:

CPU times: user 23.1 ms, sys: 0 ns, total: 23.1 ms
Wall time: 26.8 ms

На первый взгляд отличий в коде практически нет, однако по результатам производительности видно превосходство Cython почти в 25 раз.

Давайте разберемся в чем же дело.

Разница состоит в том, что Cython в отличие от Python статически компилируемый и в нем есть, скажем, синтаксический сахар, который позволяет добавить статическую информацию о типах. В примере реализации функции на Python (напомню, что данный язык динамически типизируемый) в качестве аргумента подается какое-то n, а это n может оказаться чем угодно. И из-за, казалось бы, такой мелочи Python очень медленный. Интерпретатор Python совершает очень много действий для того, чтобы выяснить что же находится в этой переменной n.

В Cython, как и в таких языках как Java, C++, явно указывается то, что переменная n имеет тип int и переменная в цикле i также имеет тип int. Из-за этих явных указаний типов в переменной и происходит ускорение кода, так как больше нужно думать о том, какой это объект, какой у него тип данных и т.д.

На краткой схеме пайплайна Cython видно, что Cython код транслируется в C/C++ код, а далее компилируется в питоновское расширение .pyd, которое потом уже может использоваться с помощью Python. Это тоже одна из причин почему Cython супербыстрый.

Возникает хороший вопрос: «как компилировать Cython?». А на самом деле компилировать его очень даже просто. В примере реализации функции на Cython вызывается команда %%Cython, которая в свою очередь компилирует Cython код в ячейке.

Как мы знаем в С/С++ необходимо использовать Visual Studio, писать, запускать, компилировать, поэтому всё это бывает довольно сложно и вопрос, который возникает в случае с компилируемым языком «насколько сложно его компилировать?». В случае с Cython – супер просто. Условно в Jupyter ноутбуке написали код, вызвали команду %%Cython и код скомпилирован – его можно использовать. Если же имеется какой-то Cython код в файле, то необходимо просто импортировать install(), вызвать его. Соответственно, когда импортируются какие-то функции из файла, то Cython автоматически скомпилирует этот код.

Код, написанный в редакторе:

def is_prime_cython(int n):
  cdef int i
  for i in range(2, n):
    if n % i == 0:
      return False
  return True

Теперь импортируем его следующей командой:

from pyximport import install
install()
from prime_check import is_prime_cython

Промежуточные итоги:

  • Cython - компилируемый язык;

  • Его синтаксис очень похож на Python, но с некоторыми дополнениями для статической типизации;

  • Статическая типизация + компиляция = скорость;

  • Писать только Python код, в случае Cython, совершенно нормально;

  • Нет никаких проблем с компиляцией.

Знатоки Numba скажут: зачем смотреть в сторону Cython, если можно просто использовать декоратор из Numba и код существенно прибавит в быстродействии.

Поговорим немного о разнице.

Напишем то же самый код для проверки числа только теперь сравним все полученные результаты:

Python

Cython

Numba

def is_prime_python(n):

  for i in range(2, n):

    if n % i == 0:

      return False

  return True

%%cython

def is_prime_cython(int n):

  cdef int i

  for i in range(2, n):

    if n % i == 0:

      return False

  return True

@numba.njit

def is_prime_python(n):

  for i in range(2, n):

    if n % i == 0:

      return False

  return True

%%time

_ = is_prime_python(6_700_417)

%%time

_ = is_prime_cython(6_700_417)

%%time

_ = is_prime_python(6_700_417)

CPU times: user 753 ms, sys: 3.75 ms, total: 756 ms

Wall time: 783 ms

CPU times: user 21.3 ms, sys: 139 µs, total: 21.5 ms

Wall time: 21.5 ms

CPU times: user 113 ms, sys: 1.18 ms, total: 114 ms

Wall time: 115 ms

Видно, что добавив декоратор @numba.njit, код ускорился почти в 5 раз и нет необходимости добавлять что-то лишнего, как в случае Cython. И зачем тогда изучать Cython, если везде можно обходиться одними декораторами от Numba?

У Cython есть одно большое преимущество перед Numba. Поскольку Cython язык, в нем есть всякие вещи по типу классов, в то время как Numba просто компилятор (не язык). Поэтому Cython может быть объектно-ориентированным, а Numba не может работать с классами нормально.

И другие плюсы Cython:

  • Cython поддерживает многомерные массивы (включая Numpy), следовательно более быстрый доступ к данным;

  • Cython выполняет множество проверок за вас: проверка границ массива, деление на ноль, переполнение;

  • Cython не ограничивается численными вычислениями.

Однако можно задаться вопросом: «зачем нам такой Python, если Cython это по сути тот же Python только быстрее и вообще он идеальный». Тем не менее и в Cython есть свои проблемы. Порой Cython код сложно отлаживать: когда допускается одна ошибка, синтаксический анализатор Cython проходит по всему коду, как лавина, и после одной ошибки возникает миллион других ошибок. Поэтому совет – смотрите самый верх (первые строчки) в сообщении об ошибке.

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

Когда пишется оптимизированный код на Cython, он начинает походить на C, а C без соответствующей культуры начинает превращаться в какую-то кашу.

Просуммируем полученные знания:

  • Cython быстрый как C;

  • Поскольку это язык, то он очень гибкий;

  • Легко оборачивать C/C++ код;

  • Отладка Cython кода может быть проблемой;

  • Не лучшая документация.

Проекты, использующие Cython

Существующие проекты, которые используют Cython подтверждают тот тезис, что Cython создан не только для каких-то математических задач. Так Quora – веб-сайт, на котором отвечают на всякие вопросы, имеет очень загруженный трафик. Чтобы избавиться от проблем, которые преследуют в Python, этот сайт решили написать на Cython. К тому же уже знакомые библиотеки Pandas, а также Scikit-Learn используют язык Cython для улучшения своего функционала.

Финальные итоги:

  • Cython – это Python с некоторыми статическими вещами для скорости;

  • Cython отлично подходит для ускорения приложений Python;

  • Упакован множеством замечательных функций: оборачивание C/C++ кода, параллельные вычисления;

  • Не ограничивается численными вычислениями.

Комментарии (4)


  1. DiligentMetal
    13.07.2022 10:58

    А насколько Cython совместим с библиотеками и с модулями, или где приведен перечень, которые работают с Cython? Меня интересует работа с jaydebeapi, os и glob.


    1. Hivemaster
      13.07.2022 20:30

      Полная совместимость. Результат работы Cython - это pyd, бинарные модули Python. Грубо говоря виртуальной машине Python без разницы выполняет ли она скрипт, или бинарный модуль.


  1. WinPooh32
    13.07.2022 10:58

    В качестве альтернативы можно посмотреть на транслятор обычного Питона (а не новый язык как в случае Cython) в Си: github.com/Nuitka/Nuitka
    Без каких-то особых проблем на нем даже смог скомпилировать такой крупный проект как youtube-dl !


  1. alextretyak
    15.07.2022 15:00
    -1

    Есть такой код на Python (который считает k-ое по счёту простое число для k = 1 000 000).

    Скрытый текст
    import math
    
    def prime():
        k = 1000000
        n = k * 17
        primes = [True] * n
        primes[0] = primes[1] = False
    
        for i in range(2, int(math.sqrt(n)) + 1):
            if not primes[i]:
                continue
            for j in range(i * i, n, i):
                primes[j] = False
    
        for i in range(n):
            if primes[i]:
                if k == 1:
                    return i
                k -= 1
    
    print(prime())
    


    Я попытался переписать его под Cython.
    Скрытый текст
    import math
    from libc.stdlib cimport malloc, free
    
    def prime():
        cdef int k = 1000000
        cdef int n = k * 17
        cdef bint *primes = <bint *>malloc(17000000 * sizeof(bint))
        cdef int i, j
        for i in range(17000000):
            primes[i] = True
        primes[0] = False
        primes[1] = False
    
        for i in range(2, int(math.sqrt(n)) + 1):
            if not primes[i]:
                continue
            for j in range(i * i, n, i):
                primes[j] = False
    
        for i in range(n):
            if primes[i]:
                if k == 1:
                    free(primes)
                    return i
                k -= 1
    


    В результате получилось всего лишь в 5 раз быстрее.
    Что не так в приведённом Cython-коде?

    (Для сравнения: оригинальный Python-код [вообще без каких-либо изменений] ускоряется посредством траспайлера Python → 11l → C++ почти в 20 раз, что сопоставимо по скорости с реализацией на C/C++.)