Является ли индексирование векторов в MATLAB неэффективным?

45

Фон

Мой вопрос мотивирован простыми наблюдениями, что несколько подрывает убеждения/предположения, часто проводимые/сделанные опытными пользователями MATLAB:

  • MATLAB очень хорошо оптимизирован, когда речь идет о встроенных функциях и основных функциях языка, таких как индексирующие векторы и матрицы.
  • Петли в MATLAB медленны (несмотря на JIT) и, как правило, следует избегать, если алгоритм может быть выражен в нативной, "векторизованной" манере.

В нижней строке: функциональность ядра MATLAB эффективна и пытается превзойти ее, используя код MATLAB, трудно, если не невозможно.

Исследование эффективности векторной индексации

Примеры кодов, показанных ниже, являются такими же фундаментальными, как и:: Я назначаю скалярное значение всем векторам. Во-первых, я выделяю пустой вектор x:

tic; x = zeros(1e8,1); toc
Elapsed time is 0.260525 seconds.

Имея x, я хотел бы установить все его записи в одно и то же значение. На практике вы сделали бы это по-другому, например, x = value*ones(1e8,1), но здесь нужно исследовать эффективность векторной индексации. Самый простой способ - написать:

tic; x(:) = 1; toc
Elapsed time is 0.094316 seconds.

Позвольте вызвать метод 1 (от значения, присвоенного x). Это кажется очень быстрым (быстрее по крайней мере, чем распределение памяти). Поскольку единственное, что я здесь делаю, - это работать с памятью, я могу оценить эффективность этого кода, вычислив полученную эффективную пропускную способность памяти и сравнив ее с пропускной способностью аппаратного обеспечения моего компьютера:

eff_bandwidth = numel(x) * 8 bytes per double * 2 / time

В приведенном выше примере я умножаюсь на 2, потому что, если SSE-потоковая передача не используется, для установки значений в памяти требуется, чтобы вектор считывался и записывался в память. В приведенном выше примере:

eff_bandwidth(1) = 1e8*8*2/0.094316 = 17 Gb/s

STREAM-эталонная пропускная способность памяти моего компьютера составляет около 17,9 Гбит/с, так что действительно - MATLAB обеспечивает почти максимальную производительность в этом случае! Пока что так хорошо.

Метод 1 подходит, если вы хотите установить для всех векторных элементов какое-то значение. Но если вы хотите получить доступ к элементам в каждой записи step, вам нужно подставить :, например, 1:step:end. Ниже приведено прямое сравнение скорости с методом 1:

tic; x(1:end) = 2; toc
Elapsed time is 0.496476 seconds.

Пока вы не ожидали, что он будет выполнять какие-либо другие действия, метод 2 явно представляет собой большую проблему: замедление фактора 5 без причины. Мое подозрение в том, что в этом случае MATLAB явно выделяет индексный вектор (1:end). Это несколько подтверждается использованием явного размера вектора вместо end:

tic; x(1:1e8) = 3; toc
Elapsed time is 0.482083 seconds.

Методы 2 и 3 выполняют одинаково плохо.

Другая возможность - явно создать индексный индекс id и использовать его для индексации x. Это дает вам самые гибкие возможности индексирования. В нашем случае:

tic;
id = 1:1e8; % colon(1,1e8);
x(id) = 4;
toc
Elapsed time is 1.208419 seconds.

Теперь это действительно что-то - 12-кратное замедление по сравнению с методом 1! Я понимаю, что он должен работать хуже, чем метод 1 из-за дополнительной памяти, используемой для id, но почему это намного хуже, чем методы 2 и 3?

Попробуйте дать петлям попробовать - как можно более безнадежным.

tic;
for i=1:numel(x)
    x(i) = 5;
end
toc
Elapsed time is 0.788944 seconds.

Большой сюрприз - цикл бьет метод vectorized 4, но все еще медленнее, чем методы 1, 2 и 3. Оказывается, что в этом конкретном случае вы можете сделать это лучше:

tic;
for i=1:1e8
    x(i) = 6;
end
toc
Elapsed time is 0.321246 seconds.

И это, пожалуй, самый причудливый результат этого исследования - написанный MATLAB цикл значительно превосходит индексирование собственного вектора. Это, конечно, не так. Заметим, что цикл JIT'ed все еще в 3 раза медленнее теоретического пика, почти полученного методом 1. Таким образом, все еще есть много возможностей для улучшения. Это просто удивительно (более сильное слово было бы более подходящим), что обычное "векторизованное" индексирование (1:end) еще медленнее.

Вопросы

  • простая индексация в MATLAB очень неэффективна (методы 2, 3 и 4 медленнее, чем метод 1), или я что-то пропустил?
  • почему метод 4 (столько) медленнее, чем методы 2 и 3?
  • Почему использование 1e8 вместо numel(x) в качестве привязки цикла ускоряет код в 2 раз?

Edit После прочтения комментария Jonas, вот еще один способ сделать это с использованием логических индексов:

tic;
id = logical(ones(1, 1e8));
x(id) = 7;
toc
Elapsed time is 0.613363 seconds.

Гораздо лучше, чем метод 4.

Для удобства:

function test

tic; x = zeros(1,1e8); toc

tic; x(:) = 1; toc
tic; x(1:end) = 2; toc
tic; x(1:1e8) = 3; toc

tic;
id = 1:1e8; % colon(1,1e8);
x(id) = 4;
toc

tic;
for i=1:numel(x)
    x(i) = 5;
end
toc

tic;
for i=1:1e8
    x(i) = 6;
end
toc

end
  • 6
    +1. Продолжайте исследовать таинственные пути Matlab :)
  • 1
    Очень интересно. Я добавил еще два к вашему сравнению: использование length(x) вместо numel(x) намного медленнее. Использование x = x*6; так же быстро, как метод 1.
Показать ещё 10 комментариев
Теги:
arrays
performance
loops
vectorization

2 ответа

12

Я могу, конечно, только спекулировать. Однако, когда я запускаю ваш тест с включенным компилятором JIT и отключен, я получаю следующие результаты:

 % with JIT   no JIT
    0.1677    0.0011 %# init
    0.0974    0.0936 %# #1 I added an assigment before this line to avoid issues with deferring
    0.4005    0.4028 %# #2
    0.4047    0.4005 %# #3
    1.1160    1.1180 %# #4
    0.8221   48.3239 %# #5 This is where "don't use loops in Matlab" comes from 
    0.3232   48.2197 %# #6
    0.5464   %# logical indexing

Разделение показывает нам, где есть увеличение скорости:

% withoutJit./withJit
    0.0067 %# w/o JIT, the memory allocation is deferred
    0.9614 %# no JIT
    1.0057 %# no JIT
    0.9897 %# no JIT
    1.0018 %# no JIT
   58.7792 %# numel
  149.2010 %# no numel

Очевидная ускоренная инициализация происходит, поскольку при выключении JIT появляется сообщение о том, что MATLAB задерживает выделение памяти до тех пор, пока оно не будет использовано, поэтому x = нули (...) ничего не делают. (спасибо, @angainor).

Методы с 1 по 4, похоже, не полезны JIT. Я полагаю, что # 4 может быть медленным из-за дополнительного тестирования входных данных в subsref, чтобы убедиться, что вход имеет правильную форму.

Результат numel может иметь какое-то отношение к тому, что сложнее компилятор справиться с неопределенным количеством итераций или с некоторыми накладными расходами из-за проверки того, соответствует ли граница цикла (считается, что нет-тестов JIT только ~ 0,1 с для этого)

Удивительно, но на R2012b на моей машине логическое индексирование работает медленнее, чем # 4.

Я думаю, что это еще раз показывает, что MathWorks проделали большую работу по ускорению кода и что "не использовать циклы" не всегда лучше, если вы пытаетесь получить самое быстрое время выполнения (по крайней мере, на данный момент). Тем не менее, я считаю, что векторизация в целом является хорошим подходом, поскольку (а) JIT не работает на более сложных циклах, и (b) обучение векторизации позволяет вам лучше понять Matlab.

Заключение. Если вы хотите скорость, используйте профилировщик и перепрофилируйте, если вы переключите версии Matlab.


Для справки я использовал следующую слегка измененную тестовую функцию

function tt = speedTest

tt = zeros(8,1);

tic; x = zeros(1,1e8); tt(1)=toc;

x(:) = 2;

tic; x(:) = 1; tt(2)=toc;
tic; x(1:end) = 2; tt(3)=toc;
tic; x(1:1e8) = 3; tt(4)=toc;

tic;
id = 1:1e8; % colon(1,1e8);
x(id) = 4;
tt(5)=toc;

tic;
for i=1:numel(x)
    x(i) = 5;
end
tt(6)=toc;

tic;
for i=1:1e8
    x(i) = 6;
end
tt(7)=toc;

%# logical indexing
tic;
id = true(1e8,1));
x(id)=7;
tt(8)=toc;
  • 2
    «Проблема» инициализации происходит из-за того, что выделение памяти и обнуление вектора занимают время. Когда JIT отключен, кажется, что MATLAB задерживает выделение памяти до тех пор, пока он не будет использован, поэтому x=zeros(...) ничего не делает на самом деле. Первый раз результат для : не 1:end . Так что MATLAB просто переместил выделение памяти в x(:)=.. Если вы добавите фиктивную строку разминки для x после выделения ( x(:)=0 ), времена будут одинаковыми. За исключением, конечно, петель ..
  • 0
    Что касается разрешения числа во время numel , конечно, некоторая работа должна быть сделана. Однако по сравнению со всей работой оно должно быть незначительным. Те же результаты получаются, если перед tic я сделаю len=numel(x) и использую его в качестве границы цикла. Это выглядит для меня как ошибка производительности. Как и все остальные наблюдения.
Показать ещё 4 комментария
7

У меня нет ответа на все проблемы, но у меня есть некоторые уточненные предположения о методах 2, 3 и 4.

Относительно методов 2 и 3. Действительно, MATLAB выделяет память для векторных индексов и заполняет ее значениями от 1 до 1e8. Чтобы понять это, давайте посмотрим, что происходит. По умолчанию MATLAB использует double в качестве своего типа данных. Выделение массива индексов принимает то же время, что и выделение x

tic; x = zeros(1e8,1); toc
Elapsed time is 0.260525 seconds.

Теперь массив индексов содержит только нули. Присвоение значения вектору x оптимальным образом, как в методе 1, занимает 0.094316 секунд. Теперь вектор индекса должен считываться из памяти, чтобы его можно было использовать при индексировании. Это дополнительные 0.094316/2 секунды. Напомним, что в x(:)=1 вектор x должен быть как прочитан, так и записан в память. Поэтому только чтение занимает половину времени. Предполагая, что это все, что сделано в x(1:end)=value, общее время методов 2 и 3 должно быть

t = 0.260525+0.094316+0.094316/2 = 0.402

Это почти правильно, но не совсем. Я могу только догадываться, но заполнение индексного вектора значениями, вероятно, выполняется как дополнительный шаг и требует дополнительных 0.094316 секунд. Следовательно, t=0.4963, который более или менее соответствует времени методов 2 и 3.

Это только предположения, но они, похоже, подтверждают, что MATLAB явно создает индексные векторы при естественной векторной индексации. Лично я считаю, что это ошибка производительности. Компилятор MATLABs JIT должен быть достаточно умным, чтобы понять эту тривиальную конструкцию и преобразовать ее в вызов правильной внутренней функции. Как и сейчас, в сегодняшней индексировании индексов ограниченной пропускной способности памяти производительность достигает около 20% теоретического пика.

Итак, если вы заботитесь о производительности, вам нужно реализовать x(1:step:end) как функцию MEX, что-то вроде

set_value(x, 1, step, 1e8, value);

Теперь это явно незаконно в MATLAB, так как вы НЕ ДОПУСКАЕТСЯ для изменения массивов в файлах MEX на месте.

Изменить В отношении метода 4 можно попытаться проанализировать производительность отдельных шагов следующим образом:

tic;
id = 1:1e8; % colon(1,1e8);
toc
tic
x(id) = 4;
toc

Elapsed time is 0.475243 seconds.
Elapsed time is 0.763450 seconds.

Первый шаг, распределение и заполнение значений индексного вектора происходит в то же время, что и методы 2 и 3. Похоже, что это слишком много - для распределения памяти и установления значений (0.260525s+0.094316s = 0.3548s) требуется не более времени, поэтому есть дополнительные накладные расходы 0.12 секунды, которые я не могу понять, Вторая часть (x(id) = 4) выглядит также очень неэффективной: для установки значений x требуется время, чтобы прочитать вектор id (0.094316s+0.094316/2s = 0.1415s) плюс некоторые проверки ошибок на id значения. Программируемый в C, два шага принимают:

create id                              0.214259
x(id) = 4                              0.219768

В коде используется проверка того, что индекс double фактически представляет целое число и что он соответствует размеру x:

tic();
id  = malloc(sizeof(double)*n);
for(i=0; i<n; i++) id[i] = i;
toc("create id");

tic();
for(i=0; i<n; i++) {
  long iid = (long)id[i];
  if(iid>=0 && iid<n && (double)iid==id[i]){
    x[iid] = 4;
  } else break;
}
toc("x(id) = 4");

Второй шаг занимает больше, чем ожидаемый 0.1415s - это связано с необходимостью проверки ошибок на значениях id. Накладные расходы кажутся слишком большими для меня - возможно, это может быть написано лучше. Тем не менее, требуемое время 0.4340s, а не 1.208419s. Что MATLAB делает под капотом - я понятия не имею. Может быть, это нужно сделать, я просто этого не вижу.

Конечно, использование doubles в качестве индексов приводит к двум дополнительным уровням служебных данных:

  • размер double в два раза больше uint32. Напомним, что пропускная способность памяти здесь является ограничивающим фактором.
  • Для того, чтобы индексировать

Метод 4 может быть записан в MATLAB с использованием целых индексов:

tic;
id = uint32(1):1e8;
toc
tic
x(id) = 8;
toc

Elapsed time is 0.327704 seconds.
Elapsed time is 0.561121 seconds.

Что явно улучшило производительность на 30% и доказывает, что нужно использовать целые числа в качестве векторных индексов. Однако накладные расходы все еще существуют.

Как я вижу сейчас, мы не можем ничего сделать для улучшения ситуации, работающей в рамках MATLAB, и нам нужно подождать, пока Mathworks не устранит эти проблемы.

  • 2
    Вы сообщили об этой ошибке производительности?
  • 0
    @ Джонас, я еще этого не делал. Я хотел спросить на SO, чтобы увидеть, если я что-то упустил. Я также надеялся, что кто-то из Mathworks может ответить - здесь было много людей из MW. Ошибка производительности не является обычной ошибкой. Код работает и дает правильные результаты. Я ожидаю, что ответ от MW будет то, что это конструктивная особенность Matlab, хотя я ожидаю, что по крайней мере некоторые из описанных проблем могут быть устранены путем улучшения JIT. Индексные векторы не должны быть явно созданы. Это простой цикл. Или, может быть, нет, а накладные расходы приходят откуда-то еще?
Показать ещё 2 комментария

Ещё вопросы

Сообщество Overcoder
Наверх
Меню