суббота, 21 июля 2018 г.

Octave. Ускорение

Octave хоть и является аналогом Matlab, сильно уступает последнему в эффективности. Однако, иногда можно попытаться ускорить расчёт. Прежде всего, нужно помнить общее для всех языков (и компьютерных систем) правило: если что-то можно сделать несколькими способами, и один из них встроен в язык (систему), то следует выбрать именно этот способ. В Octave это относится к операциям над массивами. 


Рассмотрим простой пример вычисления скалярного произведения векторов. В одном случае используем стандартную операцию "*", а во втором построим цикл, в котором будем осуществлять умножение и суммирование. 


На моей машине второй способ отработал за 0,09 секунды, в то время как первый всего за 0,0002, т.е. определённая в языке операция выполнилась на 2 порядка быстрее. 

Если, несмотря ни на что, программа работает медленно, можно попробовать её распараллелить. Для этого в Octave существует несколько пакетов, один из них - parallel. Скорее всего, его придётся отдельно установить с помощью команды

   pkg install -forge parallel

Перед началом использования пакет необходимо подключить с помощью

   pkg load parallel

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

Попробуем использовать pararrayfun() для решения той же задачи, т.е. для расчёта скалярного произведения векторов. При этом нам потребуется определить 2 функции. Первая является основной и рассчитывает произведение. Вторая вспомогательная и нужна для того, чтобы в результате получить не массив произведений, а сумму элементов этого массива.


Переменная nproc формируется при загрузке пакета parallel и определяет число доступных процессоров. Вообще, количество потоков может быть как больше, так и меньше этого числа, но рекомендуется использовать nproc или nproc-1. Строки 10-12 необязательны в общем случае, но без них программа будет работать иначе. Параметр "Vectorized" указывает на то, что входные данные можно разбивать на векторы, а не обрабатывать поэлементно. Вместе с ним обязательно следует указать "ChunksPerProc", который определяет число задач на одном процессоре. Значение 1 при 4-х процессорах означает, что вся работа будет разделена на 4 задачи, значени 10 - на 40, и т.п. Последний параметр "CumFunc" указывает на кумулятивную функцию, которой следует передать для обработке полученные результаты. Это должна быть функция 2-х аргументов, коммутативная и ассоциативная. Время работы данного процесса составило 0,05 секунды, лучше чем в случае расчёта через цикл, но простое умножение всё равно эффективнее.

Чтобы написать пример с parcellfun() представим, что у нас есть изображение в RGB формате, и мы хотим рассчитать среднюю интенсивность для каждого из каналов (например, для использования в простом классификаторе изображений). Можно построить следующее решение. Функция process() написана отдельно, она выделяет нужный слой из массива, с помощью цикла выполняет суммирование, а затем делит результат на число элементов. Я специально использовал цикл, т.к. стандартная mean() сокращает время на 2 порядка и разница реализаций становится несущественной. Аргументами функции process() являются изображение (в виде массива интенсивностей) и индекс канала. В случае parcellfun() эти элементы представлены списками.


Время работы расчёта без распараллеливания 7,28 с, при распараллеливании - 3,43 с. Как и в предыдущем примере, эффективность повысилась примерно в 2 раза.

Комментариев нет:

Отправить комментарий