Эффективная оптимизация. Что такое Cython.
За последнюю неделю почему-то часто всплывал этот вопрос, поэтому распишу всё сразу в одном месте. Описывать буду на примере питона, но общие принципы применимы ко многим языкам. Итак.
Часть первая. Правила эффективной оптимизация
- Не думайте об оптимизации пока код не дописан. Вообще! Думайте, как написать код проще и понятнее. А про оптимизацию даже не вспоминайте, пока не запустите код. Когда код запустится, проверьте насколько быстро он отрабатывает. Если он достаточно быстр — задача решена, ничего делать не нужно. Точка. И только если код отрабатывает медленнее, чем требуют условия задачи — начинайте думать об оптимизации.
- Попробуйте JIT, например PyPy, если условия это позволяют (
apt-get install pypy && pypy yourprogram.py
). Когда не хватает совсем немного производительности — JIT ускорит в несколько раз, и этого может быть достаточно. И тогда всё, задача решена. Только если это не сработало, переходите к фактической оптимизации кода. - Пройдитесь профайлером.
python -m cProfile yourprogram.py
Никогда не оптимизируйте код без профилирования. По профилю определите самые медленные куски кода. Если код слишком большой — разбейте на функции. Не нужно бросаться переписывать всё подряд. Изолируйте наиболее прожорливые куски кода, и работайте только с ними. - Выполните высокоуровневую оптимизацию найденных медленных кусков кода. Используйте более быстрые библиотеки:
gmpy2
вместо встроенной длинной арифметики,python-regex
вместо встроенного re,numpy
для матричных вычислений, и т.д. Замените dict на list. Вынесите все возможные вычисления за циклы. Наконец, оптимизируйте алгоритм, или попробуйте найти ему более быстрый аналог. Если что-то получилось — goto 2. - Cython. Расставьте типы, пройдитесь профайлером, посмотрите annotate cython-а, какой код сгенерирован, какие куски можно ускорить (он их расцвечивает)... Ещё раз подчёркиваю, низкоуровневая оптимизация — это последний этап, когда другие варианты исчерпаны.
Часть вторая. Cython
Если мы всё-таки дошли до cython-а, то... что же он такое?
Cython - это транслятор из питона в Си. Всё. Он просто генерирует код на си.
Если в файле mymodule.py написать:
def somefunc(x):
y = x*42
return y
cython mymodule.py
то он том же каталоге сгенерирует mymodule.c, в котором будет что-то вроде:
static PyObject *__pyx_pf_8mymodule_somefunc(CYTHON_UNUSED PyObject *__pyx_self, PyObject *__pyx_v_x) {
PyObject *__pyx_v_y = NULL, *__pyx_r = NULL, *__pyx_t_1 = NULL;
__pyx_t_1 = PyNumber_Multiply(__pyx_v_x, __pyx_int_42);
__Pyx_GOTREF(__pyx_t_1);
__pyx_v_y = __pyx_t_1;
__pyx_t_1 = 0;
__Pyx_XDECREF(__pyx_r);
__Pyx_INCREF(__pyx_v_y);
__pyx_r = __pyx_v_y;
__Pyx_XDECREF(__pyx_v_y);
__Pyx_XGIVEREF(__pyx_r);
return __pyx_r;
}
gcc -shared -O3 -o mymodule.so mymodule.c `python-config --cflags --ldflags`
. Нигде в остальном коде ничего менять не надо. Обычный «import mymodule» загрузит бинарный module.so так же, как загрузил бы питоновый mymodule.py.Да, cython позволяет скомпилировать питонокод. Но никаких глубоких интеллектуальных оптимизаций cython не делает. Он просто вызывает из libpython.so питоновые функции, такие как PyNumber_Multiply()
. Без питона этот код работать не будет. (в принципе, его можно собрать статически, но обычно это не имеет смысла — реальная программа всё равно будет использовать кучу внешних либ, и ещё одна библиотека роли не сыграет)
Так как все вызовы питоновых функций остались, то просто сборка cython-ом большого ускорения не даст, может, раза в два. Но! Cython-у можно указать, где использовать сишные типы вместо питоновых! В примере выше, если расставить типы:
cdef double somefunc(double x):
cdef double y = x*42
return y
cython mymodule.pyx
сгенерирует в mymodule.c код:
static double __pyx_f_8mymodule_somefunc(double __pyx_v_x) {
double __pyx_v_y, __pyx_r;
__pyx_v_y = (__pyx_v_x * 42.0);
__pyx_r = __pyx_v_y;
return __pyx_r;
}
cython -a mymodule.pyx
дополнительно сгенерирует «mymodule.html», в котором раскрасит код цветами. По нему легко смотреть, какие части кода ещё стоит оптимизировать. Но так как после расстановки типов обычным питоном такой код уже не запустится, его традиционно сохраняют в файле с расширением .pyx вместо .py.Вот так, не написав ни одной строчки на си, а просто расставив типы, медленный питоновый код превращается в быстрый сишный.
В целом, это всё.
PS: Это не все возможности cython-а. В нём можно использовать плюсовые типы, например std::vector. Причём можно даже писать: cdef vector[double] sqrs = [x*x for x in somelist]
и всё преобразование из питоновых типов в плюсовые и обратно cython возмёт на себя. Можно вызывать и внешний код на си (cdef extern from).
Есть и более тонкие оптимизации, например мелким функциям можно расставлять inline (хотя с этим и gcc обычно справляется). А ещё код, не использующий питоновые объекты, не блокирует GIL! А значит отлично подходит для многопоточных вычислений. В cython-е есть и модули для параллельных вычислений.
Да и сами .pyx файлы обычно компилируются не руками, а как часть скрипта distutils/setuptool. А в отладочных целях import pyximport; pyximport.install()
и после этого обычный import mymodule
сможет импортировать не только .py, но и .pyx файлы.
Полезные ссылки
- Расстановка типов, массивы, distutils, cythonize, annotate и вектора с примерами
- cprofile и pyximport в более подробном примере
Итого: Оптимизировать надо только когда иначе нельзя, и только то, что необходимо. Низкоуровневая оптимизация делается в последнюю очередь. Но если мы её таки делаем, то cython позволяет сделать её максимально легко — просто расставив типы.
PPS: Питон в этом не уникален. Почти во всех языки есть возможности низкоуровневой оптимизации, расширения на си через FFI/JNI/и т.д. Есть unsafe код в rust и c#. Даже в паскале и си есть ассемблерные вставки. Не удивительно, что кто-то придумал аналог и для питона. Так что эти же принципы оптимизации применимы и к другим языкам.