Python прекрасен, но что, если вам нужно что-то одновременно красивое и быстрое?

Я покажу вам, как получить ускорение в 250 раз с помощью обернутого кода C ++.

Это не вина Python, а больше всех интерпретируемых языков. Мы начинаем с написания алгоритма, который нам понятен, но ужасен по производительности. Мы можем попытаться оптимизировать код, переработав алгоритм, добавив поддержку графического процессора и т. Д. И т. Д., Но давайте посмотрим правде в глаза: оптимизация кода вручную утомительна. Разве вы не хотели, чтобы существовала какая-то волшебная ... штука ... которую можно было бы запустить через свой код, чтобы сделать его быстрее? Волшебная штука под названием… компилятор?

pybind11 - фантастическая библиотека, которую вы можете использовать для обертывания C++ кода в Python, а современный компилятор C++ - волшебный мастер оптимизации.

Самый распространенный пакет в 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/…

Здесь две папки:

  1. gibbs_sampler_library/cpp/… - это весь код C++ из предыдущей части.
  2. 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:

  1. Автоматическое преобразование, как описано здесь.
  2. Ручное преобразование, как описано здесь.

Я собираюсь рассмотреть ручное преобразование, поскольку оно более понятное. Автоматическое преобразование избавит вас от написания пары строк, но приятно видеть, что в целом можно сделать.

Чтобы использовать преобразование вручную, мы создадим новый подкласс 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 in C++ -> GibbsSampler in Python.

Таким образом, мы можем использовать то же обозначение 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 - это здорово, потому что мы можем позволить компилятору выполнять работу по оптимизации, а не загрязнять наш код, сохраняя наш алгоритм простым и чистым.

Вы можете найти весь код этого проекта здесь.

Статьи по теме в Практическое кодирование: