Как ускорить python на ubuntu
Перейти к содержимому

Как ускорить python на ubuntu

  • автор:

Форум русскоязычного сообщества Ubuntu

Страница сгенерирована за 0.056 секунд. Запросов: 25.

  • Сайт
  • Об Ubuntu
  • Скачать Ubuntu
  • Семейство Ubuntu
  • Новости
  • Форум
  • Помощь
  • Правила
  • Документация
  • Пользовательская документация
  • Официальная документация
  • Семейство Ubuntu
  • Материалы для загрузки
  • Совместимость с оборудованием
  • RSS лента
  • Сообщество
  • Наши проекты
  • Местные сообщества
  • Перевод Ubuntu
  • Тестирование
  • RSS лента

© 2012 Ubuntu-ru — Русскоязычное сообщество Ubuntu Linux.
© 2012 Canonical Ltd. Ubuntu и Canonical являются зарегистрированными торговыми знаками Canonical Ltd.

Увеличиваем скорость работы Python с Numba

Python – это интерпретируемый язык. Это означает, что код Python не компилируется напрямую в машинный код, а интерпретируется в режиме реального времени другой программой, называемой интерпретатором (в большинстве случаев CPython).

Это одна из причин, почему Python обеспечивает такую ​​большую гибкость (динамическая типизация, работает везде и т.д.) по сравнению с компилируемыми языками. Однако именно поэтому Python медленный.

На самом деле существует несколько способов разогнать код на Python. Самыми популярными из них являются:

  • использование Cython;
  • использование PyPy;
  • расширение Python с использованием C/C++.

Cython позволяет напрямую вызывать библиотечные функции C и эффективно работать с большими данными. Но обладает и минусами, главными из которых являются «свой» синтаксис, который предполагает знание C, и сложности в отладке. Код Python можно ускорить, написав нативный код, но тогда использование Cython не даст прироста скорости близкой к известному PyPy.

PyPy использует технику, известную как мета-трассировка, которая преобразует интерпретатор в компилятор трассировки JIT (Just-in-time), т.е. выполнение кода включает в себя компиляцию. PyPy имеет высокую скорость, которая не уступает Cython, рационально обращается с памятью, но несмотря на совместимость со многими базовыми библиотеками Python, PyPy поддерживает далеко не все из них, и к тому же для выполнения Python-кода могут потребоваться его некоторые изменения.

Расширение Python с помощью C/C++ дает возможность добавлять новые встроенные модули в Python без труда, однако требует умения программировать на C. С помощью таких модулей можно реализовывать новые встроенные типы объектов и вызывать библиотечные функции C.

Вышеупомянутые методы требуют использования языка, отличного от Python, или компиляции кода для его работы с Python. Эти варианты не самые удобные и не всегда просты в настройке. Возникает вопрос, как быть тем, кто абсолютно не знаком с C/C++ и не желает такого знакомства. К счастью, выход есть всегда, и пакет Numba — прекрасное решение, которое поможет значительно ускорить код, не отказываясь от дружелюбного Python.

Numba & JIT compilation

Numba – это компилятор с открытым исходным кодом, использующий подход LLVM (Low Level Virtual Machine). Numba использует компиляцию JIT (Just-in-time) – это означает, что компиляция выполняется во время выполнения кода Python, а не раньше!

Установлю Numba с помощью pip.

pip install numba

Рассмотрю простой пример с проверкой числа на простоту.

Для использования Numba нужно просто импортировать декоратор (@njit) и добавить его к функции.

import math #Импортируем njit from numba import njit def isPrime(n): for i in range(2, int(math.sqrt(n)+1)): if n % i == 0: return False return n>1 def test(n): for i in range(n): isPrime(n) #добавим numba декоратор, чтобы функция работала быстрее @njit def isPrime(n): for i in range(2, int(math.sqrt(n)+1)): if n % i == 0: return False; return n>1; @njit def test_with_numba(n): for i in range(n): isPrime(n) 

Посмотрю на результаты:

В таком представлении Numba смотрится слишком хорошим, чтобы быть правдой. Но у него наверняка есть свои недостатки.

Первый вызов функции, декорированной с использованием Numba, запускается долго. Это связано с тем, что Numba пытается выяснить типы параметров и скомпилировать функцию при первом её выполнении. Чтобы уменьшить затраты времени на компиляцию при каждом вызове программы на Python, можно записать результат компиляции функции в файловый кэш. Сделать это можно, добавив в аргументы к декоратору @njit(cache=True), и тогда последующие запуски кода будут быстрыми.

Не весь код на Python будет скомпилирован с Numba. Например, если вы используете смешанные типы для одной и той же переменной или для элементов списка, вы получите ошибку. Для контроля типов переменных в numba есть способ, позволяющий сразу определить тип функции и типы входящих переменных, например, добавив строку с нужными типами в декоратор. Проиллюстрирую типизацию на примере функции сложения с декоратором @vectorize:

import numpy as np from numba import vectorize, int64, int32, float32, float64 @vectorize([float64(float64, float64)]) def sum_numbers(x, y): return x + y 

Передавая несколько типов, следует помнить, что порядок передачи последовательности типов должен следовать от наиболее конкретных к наименее (т. е. тип с плавающей запятой одинарной точности должен описываться раньше типа с плавающей запятой двойной точности), иначе диспетчеризация на основе типов не будет работать должным образом.

@vectorize([int32(int32, int32), int64(int64, int64), float32(float32, float32), float64(float64, float64)]) def sum_numbers_multitype(x, y): return x + y 

Функция будет работать для указанных типов, однако с другими типами выдаст ошибку:

Numba создан специально с учетом Numpy и очень удобен для массивов Numpy. Как известно, Pandas основан на Numpy: поддерживает конвертацию структур данных Numpy в свои собственные структуры и наоборот. Данная особенность позволяет использовать Numba не только в паре с Numpy, но и с Pandas. Это приводит к сумасшедшей оптимизации при использовании пользовательских функций или даже при выполнении различных операций в любимой многими структуре данных pandas.DataFrame.

Рассмотрю ещё два примера:

import numpy as np import pandas as pd n = 1_000_000 df = pd.DataFrame(< 'x': np.random.random(n), 'y': 100 * np.random.random(n) >) 

Воспользуюсь декоратором @vectorize, он позволяет использовать функции Python, принимающие скалярные входные аргументы, в качестве ufuncs.

Вычислю квадрат Х в наборе данных:

from numba import vectorize def squared_without_numba(x): return x ** 2 @vectorize def squared_with_numba(x): return x ** 2 

Также посмотрю на применение Numba с методом @njit(parallel=True), который позволяет автоматически распараллелить выполнение кода в функции на разных ядрах CPU, но там, где это возможно.

from numba import njit @njit(parallel=True) def test_with_numba(x, y): n = len(x) result = np.empty(n, dtype="float64") for i, (x, y) in enumerate(zip(x, y)): result[i] = x**2 + y ** 2 return result 

Приведенные примеры показали, что Numba позволяет сократить время выполнения кода и является простым способом сделать ваш код намного быстрее без особых усилий.

Попробуйте и убедитесь в этом лично!

Ускоряем Python — 4 быстрых компилирующих транслятора для Python

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

Однако, по мере того, как язык набирает популярность, разработчики хотят создавать и быстро работающие программы на Python, поэтому за последние годы появилось несколько компиляторов Python, включая IronPython и Jython.

От переводчика: Python широко используется как для создания традиционных приложений, например, веб-сервисов, так и для приложений машинного обучения и обработки больших данных, которые используют язык только для управления потоком обработки, при этом сама обработка происходит в рамках расширений, которые реализованы на C. Однако, даже в случае последнего сценария использования, часть кода, которая выполняется в рамках Python может существенно замедлить все приложение, в связи с чем появились дополнительные способы расширения языка, например, Numba.

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

Я хотел сравнить несколько компиляторов Python на одной платформе, особенно те, которые поддерживают Python 3.x. В итоге я выбрал четыре, все они работают на Ubuntu Linux: Nuitka, PyPy, Cython и cx_Freeze.

Сравнение трансляторов Python

В качестве бенчмарка будет использоваться пакет PyStone, адаптация C-программы, которую сделал Гвидо ван Россум, создатель Python (сама C-программа была переводом Ада-программы). Я нашел версию бенчмарка от Кристофера Арндта, которая способна тестировать Python 3. Чтобы получить представление о базовой производительности, оценим производительность CPython (то есть стандартного Python) с PyStone.

Все бенчмарки при переводе были выполнены заново на CPU Intel(R) Core(TM) i5-7440HQ CPU @ 2.80GHz

$ python2 pystone.py 1000000 Pystone(1.1.1) time for 1000000 passes = 3.61152 This machine benchmarks at 276892 pystones/second $ python3 pystone.py 1000000 Pystone(1.1.1) time for 1000000 passes = 4.07254 This machine benchmarks at 245547 pystones/second

Как видите, между производительностью теста в Python 2 и 3 есть существенная разница (чем больше Pystones в секунду, тем лучше). В следующих разбивках все компиляторы используют Python 3.

Nuitka

В Ubuntu 18.04 установить Nuitka возможно с помощью APT:

$ sudo apt update $ sudo apt install nuitka clang

Результат выполнения бенчмарка для компилятора Nuitka:

# Сборка для GCC $ nuitka pystone.py $ ./pystone.exe 1000000 Pystone(1.1.1) time for 1000000 passes = 2.67537 This machine benchmarks at 373780 pystones/second # Сборка для Clang $ nuitka pystone.py --clang $ ./pystone.exe 1000000 Pystone(1.1.1) time for 1000000 passes = 2.64646 This machine benchmarks at 377863 pystones/second # Использование GCC с оптимизацией $ nuitka pystone.py --lto $ ./pystone.exe 1000000 Pystone(1.1.1) time for 1000000 passes = 2.6195 This machine benchmarks at 381753 pystones/second

Как можно видеть, Nuitka позволила получить увеличение производительности на 50%, по сравнению со стандартной реализацией Python 3.

PyPy

Гидо ван Россум однажды сказал: «Если вы хотите, чтобы ваш код работал быстрее, вам, вероятно, следует просто использовать PyPy». Я загрузил переносимые двоичные файлы в папку, а в папке bin скопировал pystone.py. Затем я запустил это так: Мы просто установили PyPy 3 с помощью Ubuntu Snap:

$ sudo snap install pypy3 --classic
$ pypy3 pystone.py 1000000 Pystone(1.1.1) time for 1000000 passes = 0.359845 This machine benchmarks at 2.77897e+06 pystones/second $ pypy3 pystone.py 1000000 Pystone(1.1.1) time for 1000000 passes = 0.26776 This machine benchmarks at 3.73469e+06 pystones/second $ pypy3 pystone.py 1000000 Pystone(1.1.1) time for 1000000 passes = 0.147859 This machine benchmarks at 6.7632e+06 pystones/second

Для данного теста результаты выполнения до «разгона» показывают более 10 кратное, а после «разгона» более чем 26 кратное ускорение производительности.

Создание исполняемого файла требует больше работы. Вы должны написать свой Python в подмножестве RPython.

Cython

Cython — это не просто компилятор для Python; это языковое надмножество языка Python, который поддерживает взаимодействие с C/C ++. CPython написан на C, поэтому это язык, который обычно хорошо сочетается с Python:

$ sudo apt install cython3 pkg-config

Сборка программы с помощью Cython немного сложна. Это не похоже на Nuitka, которая просто работает из коробки:

$ cython3 pystone.py --embed $ gcc $(python3-config --includes) pystone.c -lpython3.6m -o pystone.exe $ ./pystone.exe 1000000 Pystone(1.1.1) time for 1000000 passes = 4.8549 This machine benchmarks at 205978 pystones/second

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

Cx_Freeze

Cx_freeze — это набор скриптов и модулей для «замораживания» скриптов Python в исполняемые файлы. Установить cx_Freeze можно с помощью PIP3:

sudo pip3 install cx_Freeze --upgrade
$ cxfreeze pystone.py -O -s --target-dir dist Missing modules: ? __main__ imported from bdb, pdb ? _dummy_threading imported from dummy_threading ? _frozen_importlib imported from importlib, importlib.abc ? _frozen_importlib_external imported from importlib, importlib._bootstrap, importlib.abc ? _winapi imported from subprocess ? _winreg imported from platform ? java.lang imported from platform ? msvcrt imported from subprocess ? nt imported from ntpath, os, shutil ? org.python.core imported from copy, pickle ? os.path imported from os, pkgutil, py_compile, tracemalloc, unittest, unittest.util ? vms_lib imported from platform ? winreg imported from mimetypes, platform This is not necessarily a problem - the modules may not be needed on this platform. Copying data from package collections. Copying data from package email. Copying data from package encodings. Copying data from package html. Copying data from package http. Copying data from package importlib. Copying data from package logging. Copying data from package pydoc_data. Copying data from package unittest. Copying data from package urllib. Copying data from package xml. $ dist/pystone 1000000 Pystone(1.1.1) time for 1000000 passes = 5.35072 This machine benchmarks at 186891 pystones/second

Как можно видеть, производительность даже ниже, чем у стандартного интерпретатора CPython. Данное решение разумно использовать только для упаковки всего Python-окружения в независимый исполняемый пакет. Стоит отметить, что для этой цели можно использовать и Pyinstaller.

Заключение

Я в восторге от производительности PyPy. Компиляция была очень быстрой, и приложение работало в десятки раз быстрее аналогов и оригинального кода CPython. Если вы хотите распространять бинарный файл, выбирайте Nuitka — решение дает как ускорение, так и позволяет выполнить упаковку кода.

От переводчика: в статье рассмотрено использование решений PyPy, Nuitka, Cython и Cx_Freeze для очень простого кода, который без проблем собирается данными трансляторами. Код, используемый в реальных приложениях, может быть затруднительно скомпилировать или его производительность может стать еще хуже, чем у стандартного Python. Необходимо производить бенчмарки на том коде, который вы собираетесь распространять, поскольку синтетические бенчмарки, как в этой статье не дают представления о реальном варианте использования, который будет в вашем случае.

Как ускорить Python с помощью C-расширений. Часть 2

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

В прошлый раз мы дошли как раз до примера, с него и начнем.

Как считать sha256?

В Python-коде мы задействовали библиотеку hashlib, но ее использование в нашей C-функции выглядит неправильным. Не для того мы уходили в С-код, чтобы из него снова запускать Python-код. Возможны следующие варианты:

  • Написать свою реализацию sha256 на C.
  • Использовать готовую библиотеку.

Есть мнение, что программисты по своей натуре – люди ленивые, поэтому первый вариант лично у меня вызывает отторжение.

Рассмотрим второй вариант. Какую готовую библиотеку мы можем взять? Любую, которая соответствует нашим потребностям и имеет подходящую лицензию. Если у вас есть мощная видеокарта и библиотека, которая может вычислять хеш суммы при помощи этой карты, – это отличный вариант.

Еще более производительное решение – аппаратный майнер и библиотека для управления. К сожалению, далеко не у всех есть аппаратный майнер или мощная видеокарта, поэтому воспользуемся скромным расчетом хеш-сумм на CPU. Но это все еще не отвечает на вопрос, какую библиотеку использовать. Если вы работаете в Linux (на что я искренне надеюсь) – откройте консоль и наберите man sha256. Далее я привожу короткую выдержку из того, что появилось у меня на экране:

SHA256_INIT(3) OpenSSL SHA256_INIT(3) NAME SHA1, SHA1_Init, SHA1_Update, SHA1_Final, SHA224, SHA224_Init, SHA224_Update, SHA224_Final, SHA256, SHA256_Init, SHA256_Update, SHA256_Final, SHA384, SHA384_Init, SHA384_Update, SHA384_Final, SHA512, SHA512_Init, SHA512_Update, SHA512_Final - Secure Hash Algorithm SYNOPSIS #include . int SHA256_Init(SHA256_CTX *c); int SHA256_Update(SHA256_CTX *c, const void *data, size_t len); int SHA256_Final(unsigned char *md, SHA256_CTX *c); unsigned char *SHA256(const unsigned char *d, size_t n, unsigned char *md);

Библиотека OpenSSL умеет вычислять sha256, она есть в любом дистрибутиве Linux и использует в своей работе CPU. При виде прототипов функций появляется предложение о возможной оптимизации функции mining_mining. Мы могли бы один раз обработать строку с начальным значением и наработать от нее контекст. Затем мы сохраним наработанный контекст и для дальнейшего расчета случайного значения используем его копию. С нашей небольшой начальной строкой это не даст заметного увеличения производительности. Но если начальная строка размером несколько мегабайт – это позволит значительно выиграть время.

Парсинг входных аргументов

На Python заголовок функции mining_mining выглядел бы так:

from typing import Optional, Tuple def mining(init_value: str, printable: str, start: int, end: int, expected: str) -> Optional[Tuple[str, str, int]]: """ Производит поиск hash сумм начинающихся особым образом :param init_value: Начальная строка к которой будет добавляться случайная строка :param printable: Набор символов, используемый для формирования случайной строки :param start: Начальный номер строки :param end: Конечный номер строки :param expected: Ожидаемое начало hash суммы :return: Tuple с тремя значениями: случайная строка, хеш сумма, номер случайной строки """

Для парсинга входных аргументов в C-функции применим следующий код:

static PyObject * mining_mining(PyObject *self, PyObject *args)

Ключевым элементом здесь является вызов функции PyArg_ParseTuple, мы передаем в нее указатель на Tuple с аргументами args, форматную строку и адреса переменных, которые в случае успеха установит PyArg_ParseTuple. Форматная строка:

  • s# первый аргумент – строка, помещается в переменную init_value, длина этой строки помещается в len_init_value;
  • s# второй аргумент – строка, помещается в переменную printable, длина этой строки в len_printable;
  • K третий аргумент – int, преобразуется к типу unsigned long long и помещается в переменную start;
  • K четвертый аргумент – int, преобразуется к типу unsigned long long и помещается в переменную end;
  • s# пятый аргумент – строка, помещается в переменную expected, длина этой строки помещается в len_expected.

Если при парсинге входных аргументов произойдет ошибка – функция PyArg_ParseTuple вернет 0 и установит соответствующий объект исключения, нам же остается только вернуть NULL. Подробнее о функции PyArg_ParseTuple и форматной строке Parsing arguments and building values можно почитать здесь.

Выделение памяти

В нашей функции mining_mining только одна переменная имеет изменяемый размер и это случайная строка. Размер всех остальных переменных фиксирован и известен в момент компиляции, размещать мы их будем на стеке. Для определения максимального размера случайной строки используется функция get_max_length_value, которая выглядит следующим образом:

static size_t get_max_length_value(unsigned long long n, size_t len_printable) < return log(n) / log(len_printable) + 2; >

Максимальный размер строки определяется наибольшим номером строки end и длиной строки с символами, используемых для формирования случайной строки. Численное значение равно логарифму от end по основанию len_printable, значение округляется до целого и к нему прибавляется 2. Первая единица берется из округления, если логарифм равен 8.76 – то для строки потребуется 9 символов, еще один символ необходим для терминирующего ‘\0’. Строки в C должны заканчиваться нулевым байтом. Функция get_max_length_value используется для внутреннего использования и не имеет отображения в Python.

Выделение памяти под случайную строку:

char *value = PyMem_RawCalloc(1, get_max_length_value(end, len_printable)); if (value == NULL) return PyErr_NoMemory();

Используется функция PyAPI_FUNC(void *) PyMem_RawCalloc(size_t nelem, size_t elsize), которая предназначена для выделения памяти под массив с nelem-элементами, каждый из которых имеет размер elsize. Почему она здесь используется? Эта функция производит инициализацию выделенной памяти нулями, которые обязаны быть в конце C-строк. Если при выделении памяти произошла ошибка, то функция PyMem_RawCalloc возвращает NULL. В этом случае мы вызываем функцию PyErr_NoMemory, которая установит объект исключения и вернет NULL, который мы, как признак ошибки, вернем вызывающей функции. Если все прошло хорошо – необходимо не забыть перед выходом из функции сделать

PyMem_RawFree(value);

для того, чтобы освободить занимаемую память.

Как освободить GIL?

Настала пора победить GIL. Глобальная блокировка интерпретатора не нужна C-коду и ее можно смело отпускать. Главное условие: нельзя обращаться к Python объектам, если блокировка отпущена. Последовательность действий такая:

  1. Распарсить входные аргументы и скопировать их в C-переменные.
  2. Отпустить GIL.
  3. Произвести необходимые вычисления.
  4. Захватить GIL.
  5. Сформировать результат в виде Python объекта.
  6. Вернуть результат.

Для отпускания и получения GIL можно использовать следующие макросы:

Py_BEGIN_ALLOW_THREADS // Здесь могут находится интенсивные вычисления и блокирующие IO операции Py_END_ALLOW_THREADS

С этими макросами надо соблюдать осторожность, в макросе Py_BEGIN_ALLOW_THREADS содержится открывающая фигурная скобка, а в макросе Py_END_ALLOW_THREADS есть закрывающая скобка. Поэтому макросы должны находится на одном уровне вложенности.

Если это условие не соблюдается, то можно воспользоваться макросами:

PyThreadState *_save; Py_UNBLOCK_THREADS // Здесь могут находится интенсивные вычисления и блокирующие IO операции Py_BLOCK_THREADS

Подробнее о Thread State and the Global Interpreter Lock – здесь.

Продолжение функции mining_mining

Как уже отмечалось выше, мы будем использовать два контекста для вычисления sha256:

  • initial_context, который вычисляется от начальной строки;
  • context, это копия initial_context, в который мы добавляем значение случайной строки, финализируем его, получаем двоичное представление хеш-суммы и затем преобразуем его в текстовое представление.

Другие переменные функции mining_mining:

  • hash – двоичное представление хеш суммы;
  • hexdigest – текстовое (в виде шестнадцатеричных чисел) представление хеш-суммы;
  • value – случайная строка;
  • len_value – текущая длинна случайной строки.

Пояснения к коду:

  1. Цикл расположенный в строках 62-79, обеспечивает перебор значений от start до end.
  2. В строке 64 производится копирование initial_context в context.
  3. В строке 65 вызывается функция get_value, которая формирует случайное значение, значение записывается в переменную value, функция возвращает длину формированной строки.
  4. В строках 66-67 производится наработка контекста от случайной строки и финализация расчета хеш суммы.
  5. Цикл в строках 68-71 преобразует двоичное представление sha256 в текстовое.
  6. В строке 72 производится проверка, что начало хеш суммы соответствует ожиданиям.

Формирование результата

Если начало хеш-суммы соответствует нашим ожиданиям – захватываем GIL и формируем результат:

PyObject *res = Py_BuildValue("ssK", value, hexdigest, start);

Функция Py_BuildValue создает новый tuple, состав, которого определяется форматной строкой ssK. Первые два значения – строки. Value – случайная строка, hexdigest – хеш-сумма, третье значение – номер случайной строки, значение типа unsigned long long преобразуется к типу int. У созданного значения увеличивается счетчик ссылок и в таком виде оно возвращается вышестоящей функции.

Освобождаем память, занимаемую случайной строкой value, PyMem_RawFree(value) и возвращаем результат.

В случае, если не удалось найти хеш-сумму, также захватываем GIL, освобождаем память и возвращаем None. Для этого используем макрос Py_RETURN_NONE, он увеличивает счетчик ссылок на None и возвращает указатель на объект.

Формирование необходимых структур и инициализация модуля

Все функции, которые мы хотим импортировать в Python необходимо собрать в следующую структуру:

static PyMethodDef mining_funcs[] = < , . >;

Определение структуры PyMethodDef:

struct PyMethodDef < const char *ml_name; /* The name of the built-in function/method */ PyCFunction ml_meth; /* The C function that implements it */ int ml_flags; /* Combination of METH_xxx flags, which mostly describe the args expected by the C func */ const char *ml_doc; /* The __doc__ attribute, or NULL */ >; typedef struct PyMethodDef PyMethodDef;
  • ml_name – название функции, как она будет видна в Python;
  • ml_meth – указатель на C-функцию;
  • ml_flags – тип функции, в данном случае это функция с позиционными аргументами, METH_VARARGS;
  • ml_doc – Документация функции.

Структура, описывающая модуль:

static struct PyModuleDef mining_module = < PyModuleDef_HEAD_INIT, "mining", /* name of module */ "Documentation for mining module", /* module documentation, may be NULL */ -1, /* size module */ mining_funcs /* List funcs */ >;

Функция инициализации модуля:

PyMODINIT_FUNC PyInit_mining(void)

Надо отменить, что функция инициализации, это единственный non static элемент файла mining.c, то есть эту функцию видно за пределами файла.

Сборка модуля

Для сборки модуля используется файл setup.py:

from setuptools import setup, Extension module = Extension( 'mining', sources=['mining.c'], libraries=['crypto', 'm'], extra_compile_args=['-Wall', '-Werror', '-O2'] ) setup( name='mining', version='0.0.1', ext_modules=[module] )

Сборка осуществляется стандартным классом Extension. Для получения списка библиотек следует использовать команду:

pkg-config --libs --cflags openssl

Ее вывод будет таким: ‘-lssl -lcrypto’. В процессе сборки выяснилось, что библиотека ssl не требуется, но в функции get_max_length_value мы использовали log и для его работы необходимо подтянуть библиотеку math или, как это принято в C, m.

  • -Wall – включить все предупреждения;
  • -Werror – все предупреждения считать ошибками и прерывать компиляцию;
  • -O2 – уровень оптимизации.

Для сборки модуля нужно использовать

python setup.py build

Переменная working, функции для ее установки и сброса

В файле mining.c определена переменная working:

#include static volatile bool working = true;

Это глобальная, в рамках файла mining.c, переменная, которая предназначена для немедленного завершения workers.

Ключевое слово volatile, оно говорит компилятору, что он не должен делать предположений о ее состоянии. Переменная может быть изменена другим потоком.

Используется в двух случаях:

  • Если один из workers нашел хеш сумму с необходимыми свойствами.
  • При преждевременном завершении программы по Ctrl-C.

По умолчанию переменная working равна true и workers могут работать. Для сброса working в состояние false используется функция:

static PyObject * mining_stop_working(PyObject *self, PyObject *args)

Для того, что бы вернуть переменную working в состояние true, используется функция:

static PyObject * mining_enable_working(PyObject *self, PyObject *args)

Обе эти функции импортируются в Python и для этого добавляются в массив mining_funcs.

Головной файл blockchain.py

Исходный код blockchain.py в целом соответствует prototype_multi.py с некоторыми изменениями:

  • модуль multiprocessing заменен на ThreadPoolExecutor из модуля concurrent.futures. Нам теперь не страшен GIL.
  • Добавлен обработчик signal_handler, который завершает работу всех workers при завершении программы по Ctrl-C.
  • Не требуется очереди для получения результатов работы workers.

Результаты работы blockchain.py

Результаты работы blockchain.py представлены в файле blockchain.log. Время выполнения – 56 минут 23.663 секунды, средняя производительность 2122.755 kH/s. Это меньше времени исполнения prototype_multi.py на 5 минут 31.347 секунд. Я ожидал большего.

Почему так ? В исходном коде модуля hashlib видно, что производится попытка импорта модуля _hashlib. Если она удачна, то для расчета хеш-сумм используется он.

Модуль _hashlib написан на C и его исходный код – _hashopenssl.c. Он также использует для расчета хеш-сумм библиотеку OpenSSL. Причем перед интенсивными расчетами там тоже отпускается GIL. Для примера смотрим определение функции EVP_update.

Получается, что время в 5 минут мы выиграли исключительно за счет того, что формировали случайные строки в функции get_value на C.

В случае этого конкретного примера логично остановиться на варианте prototype_multi.py.

Что НЕ НУЖНО переписывать с Python на C

  • Верхнеуровневую логику работы приложения, разбор параметров командной строки, чтение конфигурационных файлов, логирование и то, что выполняется один раз.
  • Клиентскую работу с сетью. При сетевых операциях большая часть времени уходит на задержки, ожидание ответа удаленного сервера. Пусть 95% времени занимает ожидание ответа сервера, а 5% – время работы нашего кода. Если мы перепишем код на C и заставим его работать в пять раз быстрее – мы уменьшим в пять раз только 5%. Общая выгода от такого рефакторинга – 4%, с учетом случайности сетевых задержек ее трудно уловить.
  • Работу с базами данными и сетевыми хранилищами. Исключение – когда вы имеете дело с экзотической базой данных, для соединения с которой есть только C-библиотека. Тогда нужно написать binding к этой библиотеке на C, затем выложить его в открытый доступ, чтобы в дальнейшем ни у кого такой проблемы не было
  • Переписывать код, который уже выполняется при помощи C extensions (это как раз наш случай).

Вывод

Комплексное использование языков высокого и низкого уровней в одном проекте позволяет достичь ощутимо более высоких результатов, чем при применении одного только языка высокого уровня, а разработка идет быстрее и дешевле. Python в этом отношении не уникален, практически все промышленные языки программирования в той или иной форме позволяют использовать нативные расширения. Общим знаменателем этого метода были, есть и еще достаточно долго будут C/C++.

А вы применяете в своей работе с Python С-расширения? Или знаете другие способы ускорить Python и побороть GIL? Расскажите о своем опыте в комментариях к статье!

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *