Python прекрасен, но что, если вам нужно что-то одновременно красивое и быстрое?
Я покажу вам, как получить ускорение в 250 раз с помощью обернутого кода C ++.
Это не вина Python, а больше всех интерпретируемых языков. Мы начинаем с написания алгоритма, который нам понятен, но ужасен по производительности. Мы можем попытаться оптимизировать код, переработав алгоритм, добавив поддержку графического процессора и т. Д. И т. Д., Но давайте посмотрим правде в глаза: оптимизация кода вручную утомительна. Разве вы не хотели, чтобы существовала какая-то волшебная ... штука ... которую можно было бы запустить через свой код, чтобы сделать его быстрее? Волшебная штука под названием… компилятор?
pybind11
- фантастическая библиотека, которую вы можете использовать для обертывания C++
кода в Python
, а современный компилятор C++
- волшебный мастер оптимизации.
- Чтобы узнать, как начать работу с
Python
,C++
иpybind11
с использованиемCMake
, см. Эту статью здесь. - Учебное пособие по некоторым расширенным функциям в
pybind11
см. В этой статье здесь.
Самый распространенный пакет в Python
должен быть NumPy
- NumPy
массивы есть абсолютно везде. В C++
библиотека Armadillo сильно оптимизирована, проста в использовании и широко используется (знаете ли вы, что MLPack построен на Armadillo
?). Общие Armadillo
типы данных предназначены для матриц и векторов столбцов и строк. Скорее всего: если у вас есть алгоритм на Python, использующий NumPy
, вы легко сможете переписать его с помощью методов, присущих Armadillo
.
CARMA - это именно то, что вам нужно - библиотека, которая поможет вам переходить между Armadillo
и NumPy
типами данных. Это именно то, чем должна быть C++
библиотека - только заголовок, хорошо документированная и мощная. Вы можете скачать исходный код CARMA
здесь, а документацию - здесь.
Здесь мы будем использовать CARMA
, чтобы обернуть простой сэмплер Гиббса для модели Изинга.
Вы можете найти весь код этого проекта здесь.
Обзор CARMA
Давайте посмотрим на основные команды в CARMA
:
Аналогичные команды существуют для векторов-строк и кубов.
Для эффективности очень важно продумать, когда объект копируется или нет. Поведение по умолчанию немного сбивает с толку:
- По умолчанию не копируется все, кроме:
- Векторы столбцов / строк Armadillo в массивы NumPy копируются по умолчанию.
Чтобы изменить поведение по умолчанию, проверьте convertors.h
в CARMA
источнике. Вместо этого вы можете использовать подписи:
Алгоритм сэмплера Гиббса
Давайте рассмотрим суперпростой алгоритм сэмплера Гиббса. Сначала инициализируйте двумерную решетку из 26 вращений. Затем итеративно:
- Выберите случайный узел решетки.
- Вычислите разницу в энергии, если мы перевернем спин:
Energy diff = Energy after — energy before
. - Принять флип с вероятностью
exp(Energy diff)
, т.е. сгенерировать случайную формуr
в[0,1]
и принять флип, еслиr < min(exp(Energy diff), 1)
.
Для 2D модели Изинга с параметром связи J
и смещением b
разница энергий для переворота спина s
с соседями s_left, s_right, s_down, s_up
:
- 2 * b * s — 2 * J * s * ( s_left + s_right + s_down + s_up )
Чистый Python
Начнем с простой чистой Python
реализации семплера Гиббса. Создайте файл simple_gibbs_python.py
с содержанием:
У нас есть два метода: один возвращает случайное состояние (двумерный массив NumPy из 0 или 1), а другой принимает начальное состояние, делает его выборку и возвращает конечное состояние.
Напишем для этого простой тест. Создайте файл с именем test.py
с содержимым:
Здесь мы создаем 100x100
решетку со смещением 0
и параметром связи 1
. Мы семплируем 100 000 шагов. Ниже приведены примеры начального и конечного состояний:
Код времени дает:
Duration: 2.611175 seconds
Это слишком долго! Давайте попробуем написать тот же код в C++
и посмотрим, добьемся ли мы улучшения.
Чистый C ++
Затем давайте напишем простую библиотеку на C++
, чтобы сделать то же самое. Организуем каталог следующим образом:
gibbs_sampler_library/cpp/CMakeLists.txt gibbs_sampler_library/cpp/include/simple_gibbs gibbs_sampler_library/cpp/include/simple_gibbs_bits/gibbs_sampler.hpp gibbs_sampler_library/cpp/src/gibbs_sampler.cpp
Причина помещения всего проекта в папку cpp
внутри другой папки с именем gibbs_sampler_library
будет заключаться в том, чтобы мы могли позже обернуть его в Python
.
Файл CMake используется для создания библиотеки с именем simple_gibbs
:
Заголовочный файл:
а исходный файл:
Еще раз обратите внимание, что мы не переписывали код более разумным образом - у нас те же for
циклы и подход, что и в Python
.
Мы можем построить библиотеку с
cd gibbs_sampler_library/cpp mkdir build cd build cmake .. make make install
Также существует простой вспомогательный заголовочный файл include/simple_gibbs
:
#ifndef SIMPLE_GIBBS_H #define SIMPLE_GIBBS_H #include “simple_gibbs_bits/gibbs_sampler.hpp” #endif
так что мы можем просто использовать #include <simple_gibbs>
позже.
Теперь давайте сделаем простой тест test.cpp
для нашей библиотеки:
Мы можем снова создать это, используя файл CMake
, или просто:
g++ test.cpp -o test.o -lsimple_gibbs
Запуск дает (в среднем):
Duration: 50 milliseconds
Ух ты! Это 500x
быстрее, чем код Python! Еще раз обратите внимание, что мы не переписали код в C++
в gibbs_sampler.cpp
файле каким-либо более разумным способом - у нас те же for
циклы и подход, что и в Python
. Это волшебство оптимизации в современных C++
компиляторах дало нам такое значительное улучшение.
Это настоящая роскошь компилируемых языков, с которой не могут соперничать даже другие подходы к оптимизации в Python
. Например, мы могли бы использовать cupy
(Cuda + NumPy), чтобы воспользоваться преимуществами поддержки графического процессора, и переписать алгоритм, чтобы использовать больше векторных и матричных операций. Конечно, это повысит производительность, но это оптимизация вручную. В C++
компилятор может помочь нам оптимизировать наш код, даже если мы остаемся в неведении относительно его магии.
Но теперь мы хотим вернуть наш отличный C++
код в Python
- введите CARMA
.
Обертывание библиотеки C ++ в Python с помощью CARMA
CARMA
- отличная библиотека только для заголовков для преобразования между Armadillo
матрицами / векторами и NumPy
массивами. Давайте сразу перейдем к делу. Структура каталогов такова:
gibbs_sampler_library/CMakeLists.txt gibbs_sampler_library/python/gibbs_sampler.cpp gibbs_sampler_library/python/simple_gibbs.cpp gibbs_sampler_library/python/carma/… gibbs_sampler_library/cpp/…
Здесь две папки:
gibbs_sampler_library/cpp/…
- это весь кодC++
из предыдущей части.gibbs_sampler_library/python/carma/…
- этоCARMA
библиотека только для заголовков. Идите вперед, перейдите в репозиторий GitHub и скопируйте библиотекуinclude/carma
в каталогPython
. У тебя должно быть:
gibbs_sampler_library/python/carma/carma.h gibbs_sampler_library/python/carma/carma/arraystore.h gibbs_sampler_library/python/carma/carma/converters.h gibbs_sampler_library/python/carma/carma/nparray.h gibbs_sampler_library/python/carma/carma/utils.h
Теперь посмотрим на другие файлы. Файл CMake можно использовать для сборки библиотеки Python
:
Обратите внимание, что pybind11_add_module
заменяет обычный add_library
и имеет многие из тех же параметров. Когда мы используем здесь CMake
, мы должны указать:
cmake .. -DPYTHON_LIBRARY_DIR=”~/opt/anaconda3/lib/python3.7/site-packages” -DPYTHON_EXECUTABLE=”~/opt/anaconda3/bin/python3"
Убедитесь, что вы соответствующим образом скорректируете свои пути.
Основной точкой входа в библиотеку Python
является файл simple_gibbs.cpp
:
CARMA
пока не появлялся. Давайте изменим это в файле gibbs_sampler.cpp
.
Есть два способа конвертировать между массивами NumPy и матрицами Armadillo:
Я собираюсь рассмотреть ручное преобразование, поскольку оно более понятное. Автоматическое преобразование избавит вас от написания пары строк, но приятно видеть, что в целом можно сделать.
Чтобы использовать преобразование вручную, мы создадим новый подкласс GibbsSampler
под названием GibbsSampler_Cpp
.
- Он наследует конструктор от
GibbsSampler
, поскольку не использует Armadillo. - Первый способ:
Обратите внимание, что это то же имя, что и C++
метод arma::imat get_random_state() const
, но с обратной подписью Python
. Мы вызвали чистый метод C++
и преобразовали возвращенную матрицу обратно в NumPy
. Также обратите внимание, что мы должны импортировать #include <pybind11/NumPy.h>
, чтобы использовать py::array_t<double>
.
- Точно так же второй метод:
Здесь мы конвертируем входные данные из NumPy
в Armadillo
и выходные данные обратно из Armadillo
в NumPy
.
Наконец, мы должны обернуть библиотеку стандартным pybind11
клеевым кодом:
Обратите внимание, что мы переименовали классы с C++
на Python
:
GibbsSampler
вC++
- ›GibbsSampler_Parent
вPython
(отображается, но методы не завернуты).GibbsSampler_Cpp
inC++
->GibbsSampler
inPython
.
Таким образом, мы можем использовать то же обозначение GibbsSampler
в Python
в конце.
Полный файл python/gibbs_sampler.cpp
:
Идите вперед и создайте это:
cd gibbs_sampler_library mkdir build cd build cmake .. -DPYTHON_LIBRARY_DIR=”~/opt/anaconda3/lib/python3.7/site-packages” -DPYTHON_EXECUTABLE=”~/opt/anaconda3/bin/python3" make make install
Убедитесь, что вы скорректировали свои пути.
Протестируйте упакованную библиотеку на Python
Захватывающе! Тяжелая работа, но мы готовы протестировать нашу C++
библиотеку, упакованную в Python
. В файле test.py
из раздела «Чистый Python» выше мы можем просто изменить одну строку импорта следующим образом:
import simple_gibbs_python as gs
to
import simple_gibbs as gs
и снова запустите. Я получаю следующий результат:
Duration: 0.010237 seconds
Хлопнуть! Это 250x
быстрее даже с учетом конверсий! Чистый C++
код был 500x
быстрее, поэтому мы получаем 1/2
замедление из-за накладных расходов на (1) вызов кода C++
и (2) преобразование между NumPy
массивами и Armadillo
матрицами. Тем не менее, улучшение существенное!
Заключительные мысли
Это все для вступления. Вся заслуга CARMA
, не в последнюю очередь за отличную документацию.
В Python
доступны и другие оптимизации - суть здесь не в том, чтобы довести код Python
(или код C++
) до его предела, а в том, чтобы показать, как обычная реализация C++
может быть использована для ускорения ванильного Python
кода. Кроме того, в Python
мы сосредоточены на удобочитаемости - написании удобочитаемых алгоритмов. Использование C++
для ускорения Python
- это здорово, потому что мы можем позволить компилятору выполнять работу по оптимизации, а не загрязнять наш код, сохраняя наш алгоритм простым и чистым.
Вы можете найти весь код этого проекта здесь.
Статьи по теме в Практическое кодирование:
- Чтобы узнать, как начать работу с
Python
,C++
иpybind11
с использованиемCMake
, см. Эту статью здесь. - Учебное пособие по некоторым расширенным функциям в
pybind11
см. В этой статье здесь.