Как ускорить Python с помощью C-расширений
Привет, Хабр! Я — Игорь Алимов, ведущий разработчик группы Python в МТС Digital, работаю над продуктами Smart Rollout и B2B-портал. В этой статье я расскажу о том, как писать быстрый код на Python с использованием C-расширений и победить GIL.

На мысли об ускорении Python меня натолкнула статья о языках программирования будущего. Список перспективных (по мнению автора) языков я приводить не буду, но скажу, что языку Python в будущем было отказано. В числе недостатков Python автор выделил низкую производительность и наличие GIL. Действительно, Python не слишком быстрый и имеет блокировку, которая разрешает одновременное выполнение только одного потока инструкций. Что с этим делать? Переучиваться на Java/Go/Rust/____(нужное подчеркнуть/вписать)? Погодите, есть другие способы ускорить Python и нивелировать его недостатки.
Что это за метод?
Пишем первую реализацию на Python. На производительность не обращаем внимания, наша цель — получить результат, чтобы в дальнейшем было, с чем сравнивать. Если на этом этапе производительность нас устраивает — задача выполнена, в противном случае переходим ко второму пункту.
Пытаемся понять, где в коде мы теряем больше всего времени. В простых случаях это понятно сразу, в сложных придется прибегнуть к профилированию кода.
Производим рефакторинг кода так, чтобы выделить в виде отдельной функции, класса или модуля код, на который уходит больше всего времени. На этом этапе производим оптимизацию кода, используя параллельное исполнение и другие приемы, помогающие уменьшить время выполнения. При этом контролируем правильность результата и время. Если результат нас по-прежнему не устраивает — переходим к следующему пункту.
Переписываем проблемный код на C.
Почему именно на C? Для Python существуют порядка десяти различных способов использования нативного кода, но такой метод обеспечит наибольшую производительность при переключении между Python и нативным кодом. Язык C — это interlingua всех языков программирования. Ядро Linux, большинство системных библиотек, GTK и еще много чего написаны на нем.
Даже если библиотека написана на каком-то другом языке программирования, двоичный интерфейс (так называемое ABI) у нее C-подобный. Эталонная реализация Python также написана на C, о чем говорит ее название — CPython. Главный минус — особенности языка C: арифметика указателей, ручное управление памятью, отсутствие средств метапрограммирования. С другой стороны, С — это высокая производительность, прямой доступ к памяти и аппаратуре, стандартный интерфейс. Применение обоих языков программирования позволит использовать их сильные стороны и значительно уменьшит влияние недостатков.
Конкретный пример использования С-расширений
Постановка задачи
Есть некоторая начальная строка, нам необходимо добавить к ней один или несколько случайно выбранных символов из заданного набора, чтобы хеш полной строки имел особый вид. В нашем конкретном случае будем использовать sha256 и хеш полной строки должен начинаться с 8 нулей.
Начальная строка: 'Начальное значение!'
Набор символов для создания случайной строки: punctuation + digits + ascii_letters из модуля string
Хеш: sha256
Ожидаемое начала хеша: '00000000'
Написание первого варианта
Криптофункция хеш по определению является односторонней, поэтому единственный практический способ — это полный перебор всех сочетаний символов из заданного набора, сначала по одному символу, потом по два и так далее. Это позволит пронумеровать все возможные сочетания и написать функцию, которая по номеру возвращает сочетание символов. Первая реализация находится в файле prototype.py. Это простая однопоточная реализация, вот ее особенности:
Использование лога для вывода.
Использование argparse для парсинга аргументов командной строки.
Вводится понятие раунда, по умолчанию 100000000 хешей, по окончании раунда выводится производительность раунда в kH/s (кило хешей в секунду).
Основную работу выполняет функция mining, которая обрабатывает раунд целиком.
Функция get_value по номеру сочетания символов возвращает строку с этими символами.
Для расчета хешей используется библиотечный модуль hashlib.sha256.
Лог работы первого варианта находится в файле prototype.log.
Хеш 00000000331cb4111b0fb7fff9a9014aa45376e25b59516ff57e0789f86d98ce от строки 'Начальное значение