Горячий код на Python: Cython или переписать логику
Профайлер показал, что 80% времени уходит в один цикл. Разбираем, когда его спасёт Cython, а когда сначала нужно переписать сам алгоритм и структуры данных.

Обработка партии из 200 000 записей занимала 42 секунды. Профайлер (cProfile) показал, что 38 из них съедает один вложенный цикл, который считает попарные расстояния в чистом Python. Первое желание — переписать функцию на Cython и получить C-скорость. Иногда это работает и даёт десятки крат. Иногда — 1.3x, потому что узкое место не в интерпретаторе, а в том, что считается и какими структурами. Разберём, как отличить одно от другого, прежде чем тратить день на типизацию.
Материал про CPython 3.11–3.12 и Cython 3.x. Инвалидация кеша, JIT-компиляторы вроде PyPy и перенос на Rust через PyO3 — отдельные темы; здесь только выбор между Cython и переписыванием на самом Python.
Сначала мерить, потом решать
Прежде чем выбирать инструмент, нужно понять природу горячего участка. Ускорение через Cython помогает там, где время уходит на интерпретацию байткода: арифметика в циклах, обращения к атрибутам, вызовы функций в горячем пути. Оно не помогает, когда время уходит на другое.
- CPU в чистом Python-цикле — арифметика, индексация, сравнения по миллионам итераций. Здесь Cython реально даёт 10–100x.
- Вызовы библиотек на C — если 90% времени внутри NumPy, pandas или regex, оборачивать это в Cython бессмысленно: горячий код уже не на Python.
- Ввод-вывод — сеть, диск, база. Cython не ускорит ожидание ответа сервера ни на процент.
- Алгоритмическая сложность — если у вас O(n²) там, где хватило бы O(n log n), Cython просто сделает медленный алгоритм чуть быстрее. Переписывание алгоритма даст порядки, компиляция — единицы.
Разложить время по этим корзинам помогает связка: cProfile для функций, построчный line_profiler для конкретной горячей функции. Второй показывает время на каждой строке — сразу видно, циклическая ли арифметика виновата или один вызов в библиотеку.
Правило до оптимизации: если алгоритм квадратичный, сначала чините сложность. Cython, наложенный на плохой алгоритм, даёт быструю реализацию медленного решения.
Когда переписывание на чистом Python обгоняет Cython
Часто ускорение прячется в самом Python, без компиляции. Три типичных приёма, которые закрывают вопрос без единой строки на Cython.
Смена структуры данных
Проверка вхождения в list — O(n). В цикле по большому набору это тот самый квадратичный провал. Замена на set или dict убирает вложенный проход.
# Было: O(n*m), проверка in по списку
seen = []
result = []
for item in incoming: # n элементов
if item.id not in seen: # линейный поиск по списку
seen.append(item.id)
result.append(item)
# Стало: O(n), проверка in по множеству
seen = set()
result = []
for item in incoming:
if item.id not in seen: # поиск за O(1)
seen.add(item.id)
result.append(item)
На 200 000 записей это разница между секундами и минутами. Cython тут не нужен вообще.
Векторизация через NumPy
Если данные однородные и числовые, поэлементный цикл переносится в NumPy, где сам цикл выполняется в скомпилированном коде библиотеки.
import numpy as np
# Было: Python-цикл по парам
total = 0.0
for i in range(len(xs)):
total += xs[i] * ys[i]
# Стало: та же операция векторно
xs_arr = np.asarray(xs, dtype=np.float64)
ys_arr = np.asarray(ys, dtype=np.float64)
total = float(np.dot(xs_arr, ys_arr))
Векторизация нередко обгоняет наивный Cython-код, потому что NumPy использует оптимизированные BLAS-процедуры. Если задача укладывается в матричные операции — это почти всегда правильный ответ.
Убрать работу из горячего пути
Кэширование повторяющихся вычислений (functools.lru_cache), вынос инвариантов из цикла, замена атрибутного доступа на локальную переменную — дешёвые приёмы, которые иногда снимают половину времени до всякой компиляции.
Когда Cython оправдан
Cython выигрывает, когда алгоритм уже оптимален по сложности, векторизация невозможна (нерегулярные ветвления, зависимость итерации от предыдущей, работа с объектами), а профайлер упорно показывает интерпретацию байткода как узкое место.
Ключевой момент: сам по себе .pyx-файл без типов даёт скромный прирост. Настоящее ускорение приходит от статической типизации через cdef и отключения проверок.
# distance.pyx — Cython 3.x
# cython: boundscheck=False, wraparound=False
cdef double sum_of_squares(double[:] values):
cdef Py_ssize_t i
cdef Py_ssize_t n = values.shape[0]
cdef double total = 0.0
for i in range(n):
total += values[i] * values[i]
return total
Здесь работают три вещи: cdef double для чисел вместо Python-объектов, typed memoryview double[:] для доступа к массиву без обёрток, и директивы boundscheck=False / wraparound=False, которые убирают проверки границ и отрицательных индексов. Без типов тот же цикл даст в разы меньше. Директивы отключают защиту — выход за границу массива станет segfault, а не понятным исключением, поэтому включайте их только на проверенном коде.
Собрать модуль можно через setup.py с cythonize или через pyximport на этапе разработки. Точный синтаксис сборки под свою версию сверяйте в документации Cython по сборке — детали setup.py и опций менялись между 0.29 и 3.x.
Сравнение подходов
| Подход | Когда даёт максимум | Цена |
|---|---|---|
| Смена структуры данных | Линейный поиск в цикле, дубли, группировка | Почти бесплатно, читаемость даже растёт |
| Векторизация NumPy | Однородные числовые массивы, матричные операции | Зависимость от NumPy, память под массивы |
| Cython с типами | Оптимальный алгоритм, нерегулярные циклы, объекты | Шаг компиляции в сборке, C-компилятор в CI, сложнее отладка |
| Переписать алгоритм | Неоптимальная сложность (O(n²) вместо O(n log n)) | Время на проектирование и тесты, но эффект — порядки |
Порядок действий
- Замерьте
cProfile, найдите функцию с наибольшим суммарным временем. - Прогоните эту функцию через
line_profiler, определите горячую строку. - Оцените сложность алгоритма. Квадратичность — сначала чините её.
- Проверьте, где время: интерпретация, библиотека на C или ввод-вывод. Cython помогает только в первом случае.
- Если данные числовые и однородные — попробуйте NumPy до Cython.
- Только если алгоритм оптимален, векторизация невозможна, а узкое место — Python-цикл, беритесь за Cython с типами.
- Замерьте снова на тех же данных. Сравнивайте с тем же входом и на том же железе.
Что ломается на практике
- Cython без типов «не ускоряет». Компиляция голого
.pyдаёт 1.1–1.5x. Прирост — отcdefи memoryview, а не от факта компиляции. - Segfault вместо исключения. С
boundscheck=Falseвыход за границу массива роняет процесс без трейсбека. Отлаживайте с включёнными проверками, отключайте только в проде. - Разъезд аннотаций и реальности. Если в typed memoryview приходит массив не того dtype, поведение непредсказуемо. Приводите тип на входе явно (
np.asarray(..., dtype=np.float64)). - Усложнение сборки. Cython тянет C-компилятор в CI и на целевую платформу. Для библиотеки, которую ставят через wheel, придётся собирать колёса под каждую ОС и версию Python.
- Оптимизировали не то. Классическая ошибка — ускорили функцию, которая занимает 3% времени. Отсюда обязательный профайлинг до, а не «по ощущениям».
Что дальше
После оптимизации зафиксируйте бенчмарк как тест: замер времени горячей функции на фиксированном наборе данных, чтобы регрессия по скорости всплывала в CI, а не в проде. Если Cython не хватило и код всё ещё узкое место — следующий шаг это уже перенос на C-расширение или Rust через PyO3, но это отдельная стоимость поддержки. Начните с профайлинга и структур данных: в большинстве случаев до Cython дело не доходит.
Создатель презентаций: Структура и текст каждого слайда: заголовки-утверждения, одна мысль на слайд, заметки докладчику.
Спросить за 150 ₽Источники: Cython documentation — Building Cython code, Python cProfile — The Python Profilers
Частые вопросы
У меня Cython 0.29, а не 3.x — примеры сработают?
Cython скомпилировался, но ускорения почти нет — что не так?
Стоит ли всё переписать на Cython сразу для скорости?
Чем Cython отличается от Numba для числовых циклов?
Как понять, что виноват алгоритм, а не интерпретатор?
Cython усложнит деплой библиотеки?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.