Cython: более чем 30-кратное ускорение Python-кода

Автор оригинала: George Seif
  • Перевод
Python — это язык, который любят многие программисты. Этим языком невероятно легко пользоваться. Всё дело в том, что код, написанный на Python, отличается интуитивной понятностью и хорошей читабельностью. Однако в разговорах о Python часто можно слышать одну и ту же жалобу на этот язык. Особенно тогда, когда о Python говорят знатоки языка C. Вот как она звучит: «Python — это медленно». И те, кто так говорят, не грешат против истины.

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



Есть несколько способов ускорения Python-программ. Например, можно применять библиотеки, рассчитанные на использование нескольких ядер процессора. Тем, кто работает с Numpy, Pandas или Scikit-Learn, можно посоветовать взглянуть на программный комплекс Rapids, позволяющий задействовать GPU при проведении научных расчётов.

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

Но как быть в том случае, если ваш код — это чистый Python? Что если у вас есть большой цикл for, который вам совершенно необходимо использовать, и выполнение которого просто нельзя распараллелить из-за того, что обрабатываемые в нём данные должны обрабатываться последовательно? Можно ли как-то ускорить сам Python?

Ответ на этот вопрос даёт Cython — проект, используя который можно значительно ускорить код, написанный на Python.

Что такое Cython?


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

Единственное изменение Python-кода при этом заключается в добавлении к каждой переменной информации об её типе. При написании обычного кода на Python переменную можно объявить так:

x = 0.5

При использовании Cython при объявлении переменной нужно указать её тип:

cdef float x = 0.5

Эта конструкция сообщает Cython о том, что переменная представляет собой число с плавающей точкой. По такому же принципу объявляют переменные и в C. При использовании обычного Python типы переменных определяются динамически. Явное объявление типов, применяемое в Cython — это то, что делает возможным преобразование Python-кода в C-код. Дело в том, что в C необходимо явное объявление типов переменных.

Установка Cython предельно проста:

pip install cython

Типы в Cython


При использовании Cython можно выделить два набора типов. Один — для переменных, второй — для функций.

Если речь идёт о переменных, то тут нам доступны следующие типы:

  • cdef int a, b, c
  • cdef char *s
  • cdef float x = 0.5 (число одинарной точности)
  • cdef double x = 63.4 (число двойной точности)
  • cdef list names
  • cdef dict goals_for_each_play
  • cdef object card_deck

Обратите внимание на то, что тут, фактически, показаны типы C/C++!

При работе с функциями нам доступны следующие типы:

  • def — обычная Python-функция, вызывается только из Python.
  • cdef — Cython-функция, которую нельзя вызвать из обычного Python-кода. Такие функции можно вызывать только в пределах Cython-кода.
  • cpdef — Функция, доступ к которой можно получить и из C, и из Python.

Теперь, когда мы разобрались с типами Python, можно заняться ускорением Python-кода.

Ускорение кода с использованием Cython


Начнём с создания Python-бенчмарка. Это будет цикл for, в котором выполняется вычисление факториала числа. Соответствующий код на чистом Python будет выглядеть так:

def test(x):
    y = 1
    for i in range(1, x+1):
        y *= i
    return y

Cython-эквивалент этой функции очень похож на её исходный вариант. Соответствующий код нужно поместить в файл с расширением .pyx. Единственное изменение, которое нужно внести в код, заключается в добавлении в него сведений о типах переменных и функции:

cpdef int test(int x):
    cdef int y = 1
    cdef int i
    for i in range(1, x+1):
        y *= i
    return y

Обратите внимание на то, что перед функцией стоит ключевое слово cpdef. Это позволяет вызывать данную функцию из Python. Кроме того, тип назначен и переменной i, играющей роль счётчика цикла. Не будем забывать о том, что типизировать нужно все переменные, объявленные в функции. Это позволит компилятору C узнать о том, какие именно типы ему использовать.

Теперь создадим файл setup.py, который поможет нам преобразовать Cython-код в C-код:

from distutils.core import setup
from Cython.Build import cythonize

setup(ext_modules = cythonize('run_cython.pyx'))

Выполним компиляцию:

python setup.py build_ext --inplace

Теперь С-код готов к использованию.

Если взглянуть в папку, в которой находится Cython-код, там можно будет найти все файлы, необходимые для запуска C-кода, включая файл run_cython.c. Если вам интересно — откройте этот файл и посмотрите на то, какой С-код сгенерировал Cython.

Теперь всё готово к тестированию нашего сверхбыстрого C-кода. Ниже приведён код, используемый для тестирования и сравнения двух вариантов программы.

import run_python
import run_cython
import time

number = 10

start = time.time()
run_python.test(number)
end =  time.time()

py_time = end - start
print("Python time = {}".format(py_time))

start = time.time()
run_cython.test(number)
end =  time.time()

cy_time = end - start
print("Cython time = {}".format(cy_time))

print("Speedup = {}".format(py_time / cy_time))

Код этот устроен очень просто. Мы импортируем необходимые файлы — так же, как импортируются обычные Python-файлы, после чего вызываем соответствующие функции, делая это так же, как если бы мы всё время работали бы с обычными Python-функциями.

Взгляните на следующую таблицу. Можно заметить, что Cython-версия программы оказывается быстрей её Python-версии во всех случаях. Чем масштабнее задача — тем больше и ускорение, которое даёт использование Cython.
Число
Показатель Python Time
Показатель Cython Time
Показатель Speedup
10
1.6689300537109375e-06
4.76837158203125e-07
3.5
100
3.337860107421875e-06
4.76837158203125e-07
7.0
1000
2.193450927734375e-05
9.5367431640625e-07
23.0
10000
0.0002090930938720703
6.4373016357421875e-06
32.481
100000
0.0021562576293945312
6.008148193359375e-05
35.89
1000000
0.02128767967224121
0.0005953311920166016
35.75
10000000
0.2148280143737793
0.00594782829284668
36.1187317112278

Итоги


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

Уважаемые читатели! Используете ли вы Cython в своих проектах?

RUVDS.com
1 072,98
RUVDS – хостинг VDS/VPS серверов
Поделиться публикацией

Комментарии 27

    +7
    Cython это правильный олдскульный вариант для ускорения Python кода. Но я давно всем рекомендую Numba.

    Одним декоратором можно добиться такой же скорости, как и у Cython без всяких .pyx модулей и С-подобного языка. Вот рабочий код для факториала:

    from numba import jit
    
    @jit
    def test(x):
        y = 1
        for i in range(1, x+1):
            y *= i
        return y
    
      +8
      pypy, кстати, тоже олдскульный вариант для ускорения, также не требует издеваться над кодом в питоне чтобы он был похож на C. Уже лучше по-моему чистый С код или C extensions.
        +4
        Бешено плюсую Numba для численных вычислений в цикле и особенно в цикле в цикле.
        Она даже больше чем Cython ускоряет в каких-то случаях, потому что может параллельно выполнять код и другие трюки использовать.
          0
          Для простых вычислительных функций numba идеальный вариант. Трудности возникают при использовании разнородных и сложноструктурированных даннх к которым numba плохо приспособлен. Да, там есть jitclass но он оставляет желать лучшего. Если взаимодействующих объектов много то со временем становится сложно понять какой код генерит numba и откуда валятся ошибки. В этот момент задумываешься о преимуществах Cython где все декларируется явным образом. Если хорошо знаком с C++ то он может быть предпочтительным вариантом несмотря на бОльший входной порог.
          +1
          Нет, не использую. В моих Python проектах bottleneck это не Python, а скорее нетворк, вообще I/O и т.д. Ускорять Python будет пустой тратой времени.
          Но если вдруг понадобится посчитать факториал, буду знать :)
            +1
            Для факториала таки лучше выше упомянутая Numba )
              +1
              Небольшое уточнение: для факториала аргумента, не большего 25.
            –1
            Единственное изменение Python-кода при этом заключается в добавлении к каждой переменной информации об её типе.

            Если требуется пройти по всему коду, то может лучше тогда заодно на go переписать?

              +24

              Боже, что вы намерили? Результат функции test(100000) — это число с 456574 знаками. Не число 456574, а число с 456574 знаками.


              $ ipython
              Python 3.7.3 (default, Apr  3 2019, 19:16:38) 
              Type 'copyright', 'credits' or 'license' for more information
              IPython 7.4.0 -- An enhanced Interactive Python. Type '?' for help.
              
              In [1]: def test(x): 
                 ...:     y = 1 
                 ...:     for i in range(1, x+1): 
                 ...:         y *= i 
                 ...:     return y 
                 ...:                                                                                                                         
              
              In [2]: %time a = test(100000)                                                                                                  
              CPU times: user 1.76 s, sys: 3.57 ms, total: 1.76 s
              Wall time: 1.76 s
              
              In [3]: len(str(a))                                                                                                             
              Out[3]: 456574

              Время работы от увеличения количество итераций должно расти явно не линейно, так как длина арифметики тоже увеличивается. Каким образом у вас это заняло 2 миллисекунды?


              А в Cython тип int — это 32-bit signed integer: https://nyu-cds.github.io/python-cython/01-syntax/
              Соответственно результат работы функции на Cython просто неверный.

                +2

                Если вы имели в виду, что результат должен быть типа double (то есть y = 1.0), то результат будет бесконечность уже для test(171).

                  +1

                  Коммент об этом есть к оригиналу сего шедевра.

                    +13

                    Разгадка, если кому интересно.


                    В оригинальном посте на Медиуме была ошибка, из-за которой результат функции всегда был ноль!


                    def test(x):
                        y = 1
                        for i in range(x):
                            y *= i
                        return y

                    Замеры были сделаны именно для этой версии, и тут нет длинной арифметики.


                    Автор оригинального поста решил не париться и убрал неверный код и результаты, оставив голословные утверждения о 36-кратном приросте производительности непонятно чего и упоминание некой таблицы, которой в посте нет:


                    Check out the table below which shows how much speed Cython gave us for different factorial values. We got over 36X speedup with Cython!

                    Авторы же перевода видимо захотели исправить код, получили работающую в тысячи раз медленнее версию на питоне и неверно работающую версию на Cython.

                      0
                      Никогда не писал на питоне и не знаком с языком вообще, но ситуацию, когда трансляция кода в С будет работать не так, как хотелось и как работал бы оригинал, предвидел сразу. С таким же успехом можно написать свой «компилятор» для любого языка, наделав и спрятав кучу ошибок.
                    +2
                    Наверно лучше сделать C dll и вынести тяжелый цикл в нее. Я так у себе сделал когда надо было бегать по двойному циклу, и python задумывался. Конечно время зависело от железа. На мощных ПК может и не особо, а вот на слабых машинах очень и очень долго. Вынеся его в dll отработка цикла происходит очень быстро и главное даже на слабых ПК работает так же быстро.
                      +3
                      Не стоит думать о Cython как о «быстром Python». Это скорее C похожим на Python синтаксисом.
                        –2
                        Всегда когда я пробывал использовать CPython он или не работал, или не давал разницы больше 5%. Может в какихто тяжелых вычислениях он и показывает это преимущество, но мне их тогда проще написать на C и включить модулем.
                        С puppy та же петрушка, но при использовании falcon pypy таки дает гдето 10-20%(из обещаных 200+)
                          0
                          Всегда при упоминании Cython задавал себе вопрос, а нельзя разве написать такой инструмент, чтобы автоматически преобразовывать питоновский код в ситоновский? Ведь это должно быть просто, если на самом деле разница только в объявлении типов.
                            +3
                            Там такая разница, что от Питона остается только хвостик )
                              0
                              Вполне может быть. Но в статье почему то говорится, что
                              Единственное изменение Python-кода при этом заключается в добавлении к каждой переменной информации об её типе.

                              То есть, вроде как всего ничего.
                              0
                              Так pypy же в теории пытаются это сделать. Но чегото не особо получается.
                                +3
                                В питоне типы не известны статически, поэтому «разница только в объявлении типов» — это не только не просто, но и нерешаемо в общем случае.
                                  0

                                  Известны почти всегда, если код соблюдает PEP 484.
                                  Но этот PEP в python-комьюнити презирают, поддерживается единичными библиотеками.

                                    –1
                                    Если по уму такой транслятор делать, то он тог бы такие переменные отслеживать и создавать для каждого типа новые. Ну или хотя бы выдавать ошибку в месте, где тип меняется. Так как это, всё-таки, не очень хорошо, менять тип переменной по ходу выполнения.
                                    0

                                    Cython и пытается это делать, можно python код без изменений оставить, но будет работать медленнее:
                                    http://docs.cython.org/en/latest/src/tutorial/pure.html


                                    In some cases, it’s desirable to speed up Python code without losing the ability to run it with the Python interpreter. While pure Python scripts can be compiled with Cython, it usually results only in a speed gain of about 20%-50%.
                                    0
                                        cdef int i
                                        for i in range(1, x+1):
                                            y *= i
                                    А после такого объявления переменная будет видна после цикла или нет?
                                      0
                                      Будет видна, конечно. А к чему ваш вопрос, интересно?
                                        0
                                        К тому, что я тупой. Просто почему-то подумал, что в питончике переменная цикла видна только внутри цикла. Можете не обращать внимания.

                                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                    Самое читаемое