Привет, Хабр! Я – Игорь Алимов, ведущий разработчик группы 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 от строки 'Начальное значение
domix32
26.05.2022 00:11+1А wasm модули собирать не пробовали? Или на расте либу написать?

igoral Автор
26.05.2022 09:09Написать расширение для Python на ассемблере ? Да, пожалуй это можно, но потребуется выполнить требования Python API, а так почему бы нет ?
Про Rust к сожалению, ничего не могу сказать, я сам с ним знаком на уровне Hello world.

domix32
26.05.2022 15:09+1Не совсем. Написать библиотеку на том же Си без ограничений на Python - ни тебе с GC возиться, ни типы не прокидывать. Библиотека компилируется в wasm-модуль и из питона подключается что-то типа
from wasm import vm # название вымышлено, реальная либа звалась иначе lib = vm.loadlib('hasher') lib.sha256(input())Теоретически скорость должна быть на уровне нативного кода, за исключением накладных расходов на передачу данных между wasm и python.

igoral Автор
27.05.2022 09:44Пожалуй, надо признаться, что про wasm я знаю еще меньше, чем про Rust.

StasTukalo
26.05.2022 17:09+1Есть некоторая начальная строка, нам необходимо добавить к ней один или несколько случайно выбранных символов из заданного набора, чтобы хеш полной строки имел особый вид. В нашем конкретном случае будем использовать sha256
Рисёчеры в МТС работают над перебором ключей к биткойн-кошелькам? ))

igoral Автор
27.05.2022 09:36Рисёчеры в МТС работают над перебором ключей к биткойн-кошелькам? ))
К сожалению, нет. Я старался выбрать пример, требующих интенсивных вычислений, который будет понятен широкой аудитории.
mr-garrick
Для чего #define PY_SSIZE_T_CLEAN ?
igoral Автор
Макрос PY_SSIZE_T_CLEAN определяется перед импортом Python.h для правильного типа размера данных. Если определен макрос PY_SSIZE_T_CLEAN то тип размера данных будет Py_ssize_t, иначе int. Это особенно важно на 64 битных системах, где размер данных может превышать 2**31. Подробнее об этом Parsing arguments and building values первое примечание.