Эффективное зацикливание в MATLAB/Octave
Порядок массива и функция permute()
Порядок массива является важным фактором при циклическом просмотре данных в MATLAB/Octave. Выбор того, какое измерение использовать в цикле, может существенно повлиять на эффективность вычислений. В этом руководстве я представляю и иллюстрирую концепцию порядка массива, создаю простой эксперимент, чтобы продемонстрировать влияние порядка массива при циклировании, и объясняю, как можно использовать команду permute() в MATLAB для записи эффективные циклы каждый раз.
Весь исходный код примеров можно найти в репозитории GitHub.
Порядок массива
При работе с многомерными или N-мерными (ND) массивами важно знать, как данные хранятся в памяти. Это называется упорядочением массива. Существует два основных соглашения: порядок по строкам и по столбцам. Эти два разных метода просто относятся к тому, какие элементы области массива ND непрерывны в памяти, а также к тому, как такие массивы должны быть линейно индексированы. В порядке столбцов элементы столбцов являются смежными в памяти. и в порядке строк элементы строк являются смежными в памяти. Таким образом, в основном мы считаем элементы по столбцам для столбцов и по строкам для строк.
В следующем фрагменте кода показан пример матрицы 3 x 3, где каждый элемент показывает, что такое линейный индекс. Точно так же вы можете думать о числах в матрице как о порядке данных в памяти (например, 1 — это первый адрес памяти, 2 — второй адрес памяти и т. д.).
colMaj = [ 1, 4, 7; 2, 5, 8; 3, 6, 9 ]; disp( colMaj ) 1 4 7 2 5 8 3 6 9 rowMaj = [ 1:3; 4:6; 7:9 ]; 1 2 3 4 5 6 7 8 9 disp( rowMaj )
Мы можем тривиально увидеть, как MATLAB хранит данные, используя оператор «:», который разворачивает данные. Поскольку MATLAB работает с основными столбцами, вы можете видеть, что матрица colMaj возвращает индекс в правильном порядке, а матрица rowMaj возвращает элементы не по порядку.
disp( colMaj(:) ) 1 2 3 4 5 6 7 8 9 disp( rowMaj(:) ) 1 4 7 2 5 8 3 6 9
Чтобы сделать это более понятным, мы можем создать простую анимацию. Анимация показывает, как разворачивается матрица с использованием обоих соглашений. Линейный индекс внизу показывает, как отдельные элементы фактически хранятся в памяти.
Перебор данных
Итак, почему порядок массива имеет значение? Ну, когда вы перебираете данные в цикле, порядок массива может существенно повлиять на время выполнения. Гораздо эффективнее обращаться к непрерывным данным в памяти. Таким образом, знание того, как наши данные организованы в памяти, также может помочь нам написать более эффективный в вычислительном отношении код.
Продемонстрируем это на простом примере. Предположим, у нас есть набор изображений, с которыми мы хотим работать. Изображения имеют размер (nX, nY) пикселей, и в нашем наборе данных есть изображения nI. Возможны два варианта расположения этих изображений в памяти: [nX, nY, nI] или [nI, nX, nY]. В первой ориентации изображения будут располагаться в памяти последовательно, а во второй ориентации — нет.
Чтобы увидеть, какая разница в порядке массива, мы можем написать следующий тестовый код. Обратите внимание, что функции tic() и toc() позволяют нам определять время выполнения частей нашего кода. Если вы не знакомы с этими функциями, tic() фактически запускает таймер и toc() записывает, сколько времени прошло с момента запуска таймера. Дополнительную документацию можно найти в справке по MATLAB.
% Arrange images at the first two dimension imageData = randn( nX, nY, nI ); a = tic(); for ix1=1 : nI thisImg = imageData( :, :, ix1 ); end et1 = toc( a ); % Arrange images at the last two dimensions imageData = randn( nI, nX, nY ); a = tic(); for ix1=1 : nI thisImg = imageData( ix1, :, : ); end et2 = toc( a ); % Plot the results fprintf( 'Looping over the last dimension: %0.4f seconds\n', et1 ); fprintf( 'Looping over the first dimension: %0.4f seconds\n', et2 ); fprintf( '%0.1f factor increase\n', et2/et1 );
Чтобы проиллюстрировать влияние порядка массива, код был запущен с nX = nY = 128 и nI со значениями 1 000, 10 000 и 50 000. Смелым и любознательным предлагается скачать исходный код и опробовать более интересные комбинации!
Начав всего с 1000 изображений, мы видим, что порядок массива не оказывает большого влияния, однако время выполнения все еще увеличивается примерно на 10%.
Looping over the last dimension: 0.8328 seconds Looping over the first dimension: 0.9570 seconds 1.1 factor increase
Увеличив количество изображений до 10 000, мы начинаем видеть гораздо более существенную разницу во времени выполнения. Время выполнения увеличивается в 2,4 раза, когда изображения не хранятся в памяти непрерывно.
Looping over the last dimension: 0.9039 seconds Looping over the first dimension: 2.1613 seconds 2.4 factor increase
При дальнейшем увеличении количества изображений до 50 000 мы по-прежнему наблюдаем значительное увеличение времени выполнения. Теперь время выполнения увеличено в 8 раз!
Looping over the last dimension: 1.2299 seconds Looping over the first dimension: 10.3527 seconds 8.4 factor increase
Важно отметить, что это был простой пример с одной петлей и только тремя измерениями. Этот эффект на самом деле может быть намного хуже для массивов с большим количеством измерений или когда мы вкладываем несколько циклов. Иногда имеет смысл добавить несколько вызовов tic() / toc() в свой код или запустить Profiler там, где вы, возможно, захотите внести некоторые изменения в свой код.
Переставить() в MATLAB
MATLAB включает функцию под названием permute(), которая является обобщением функции транспонирования, но для массивов ND. Permute() принимает массив ND и желаемый порядок массива, а затем возвращает переупорядоченные данные. Синтаксис выглядит следующим образом: newArray = permute( oldArray, [oldDim1, oldDim2, oldIm3 и т. д.]). В этом синтаксисе переменные oldDim# представляют собой порядковый индекс старого массива, а позиция во входных данных функции представляет новую порядковую позицию массива.
Например, предположим, что у нас есть ND-массив A формы [200, 450, 120, 680] и мы вызвали функцию permute() как таковую: B = перестановка( A, [3, 1, 4, 2]). Результирующая форма массива B будет [120, 200, 680, 450]. Здесь мы переместили измерение 3 в измерение 1, измерение 1 в измерение 2, измерение 4 в измерение 3 и измерение 2 в измерение 4.
Используя функцию permute(), мы можем переупорядочить любой ND-массив в зависимости от того, какое измерение мы хотим перебрать. Возвращаясь к нашему предыдущему примеру с изображением, мы можем взять наш массив imageData формы [nI, nX, nY] и преобразовать его в массив формы [nI , nX, nY] с помощью следующей команды.
imageData = permute( imageData, [ 2, 3, 1 ] );
Теперь это позволит нам перебрать последнее измерение, которое, как мы только что показали, более эффективно с точки зрения вычислений. Добавив эту простую строку в наш предыдущий пример, мы можем повторно запустить наш тест. Обновленный код будет выглядеть следующим образом.
% Arrange images at the first two dimension imageData = randn( nX, nY, nI ); a = tic(); for ix1=1 : nI thisImg = imageData( :, :, ix1 ); end et1 = toc( a ); % Permute data before looping imageData = randn( nI, nX, nY ); imageData = permute( imageData, [ 2, 3, 1 ] ); % loop over the last dim a = tic(); for ix1=1 : nI thisImg = imageData( :, :, ix1 ); end et2 = toc( a ); % Plot the results fprintf( 'Looping over the last dimension: %0.4f seconds\n', et1 ); fprintf( 'Permute and loop over the last dimension: %0.4f seconds\n', et2 ); fprintf( '%0.1f factor increase\n', et2/et1 );
Выполнение этого эксперимента с nI = 50 000 приводит к следующему результату.
Looping over the last dimension: 1.0844 seconds Permute and loop over the last dimension: 1.1350 seconds 1.0 factor increase
Использование команды permute() значительно сократило время цикла. Конечно, использование функции перестановки сопряжено с небольшими накладными расходами, но почти во всех практических случаях ее лучше включать. Как я упоминал ранее, это особенно верно для многомерных массивов и когда мы вкладываем несколько циклов.
Резюме
В этом уроке мы рассмотрели важность порядка массивов при циклическом обходе ND-массивов в MATLAB/Octave. Используя простой код MATLAB, мы продемонстрировали преимущества циклического воспроизведения данных, расположенных в памяти непрерывно. Далее мы показали, что команда permute() позволяет нам переупорядочивать размеры массива перед циклом, чтобы мы всегда могли убедиться, что цикл выполняется эффективно.
Вот несколько моментов, которые следует помнить:
- Данные расположены в порядке столбцов в MATLAB
- Мы всегда должны перебирать самые внешние измерения, чтобы сделать наши циклы максимально эффективными.
- Permute() можно использовать до (и после) любого цикла, чтобы обеспечить эффективный цикл по ND-массивам.
- Для вложенных циклов самый внутренний цикл должен зацикливаться на самом внешнем измерении.
Удачного кодирования!