Из этой серии:

  1. Пример кластеризации среднего сдвига в пространстве Пуанкаре
  2. Векторизация циклов с помощью Numpy
  3. Пакеты и многопоточность
  4. Своевременная компиляция с помощью Numba (этот пост)

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

Итак, пора переходить к грубой силе. Но все же я не буду использовать GPU или TPU. Это потому, что, как я уже показал в этом другом посте, часто вы теряете так много времени при перемещении данных из системной памяти в GPU (или TPU), что, в конце концов, весь процесс будет медленнее.

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

Одно из них - Нумба. Это инструмент, который берет наши функции и компилирует их, что означает, что он переводит их на низкий уровень и код, который (грубо говоря) говорит на том же языке, что и ЦП. И так быстрее.

Нумба на работе

Попросить Numba скомпилировать функцию просто, написав четыре буквы перед определением функции, «@jit»:

Символ at в Python известен как декоратор функций. Они говорят интерпретатору изменить функциональность определенной функции. В нашем случае декоратор говорит интерпретатору изменить функцию, следуя инструкциям Numba. Более подробную информацию о таких тонкостях можно найти здесь.

На самом деле в Numba есть много очень полезных инструментов. Один за всех: он может выполнять автоматическое распараллеливание кода. Конечно, «автоматический» инструмент, как правило, не может справиться со всеми возможными видами распараллеливания в функции, поэтому не ждите от него чудес. Поскольку нам уже удалось вручную распараллелить код, мы не будем использовать этот функционал.

Значит, нам просто нужно добавить декоратор перед определениями функций? Почти. Поскольку Numba все еще находится в стадии разработки, поддерживаются не все функции Numpy, которые мы используем. Итак, мы должны внести некоторые незначительные изменения.

В частности, функция np.tile по-прежнему не поддерживается. Простой обходной путь - создать пустой массив желаемой формы и заполнить его копиями векторов, которые нам нужны:

Вы можете видеть, что мы кормим декоратор двумя параметрами:

  • Первый, nopython = True, сообщает Numba, что нужно скомпилировать функцию. Это может показаться избыточным, и на самом деле в нашем случае это так. Но в тех случаях, когда Numba не может скомпилировать функцию, она будет выполняться обычным способом, интерпретируемым Python, и поэтому мы потеряем функциональность, которую хотим достичь. Добавляя этот параметр, Numba будет вызывать ошибку в таких случаях, и поэтому мы можем покопаться, чтобы понять, где она падает. Так что это хорошая практика в любом случае добавить его.
  • Второй, nogil = True, сообщает интерпретатору Python освободить Global Interpreter Lock. Описание функциональности GIL выходит за рамки этой публикации, но в вкратце: если мы сохраним его вместе с скомпилированным кодом, мы потеряем распараллеливание, поэтому мы должны его выпустить.

Остальной код точно такой же, как в предыдущем посте, за исключением размещения декоратора с двумя аргументами, @jit (nopython = True, nogil = True) в двух других. вызываемая функция (__shift, gaussian). Обратите внимание, что основной, meanshift_parallel, не может быть скомпилирован так, как он построен.

Повышение производительности

А теперь перейдем к цифрам. Сколько скорости мы набрали?

Что ж, мы фактически выиграли время исполнения. Но есть эффект насыщения: с ростом числа векторов скорость выполнения двух приближается все больше и больше.

Это последний пост из этой серии.

Спасибо за чтение!