arrayfun может быть значительно медленнее, чем явный цикл в matlab. Зачем?


рассмотрим следующий простой тест скорости arrayfun:

T = 4000;
N = 500;
x = randn(T, N);
Func1 = @(a) (3*a^2 + 2*a - 1);

tic
Soln1 = ones(T, N);
for t = 1:T
    for n = 1:N
        Soln1(t, n) = Func1(x(t, n));
    end
end
toc

tic
Soln2 = arrayfun(Func1, x);
toc

на моей машине (Matlab 2011b на Linux Mint 12) Выход этого теста:

Elapsed time is 1.020689 seconds.
Elapsed time is 9.248388 seconds.

что?!? arrayfun, хотя, по общему признанию, более чистое решение, на порядок медленнее. Что здесь происходит?

далее, я сделал аналогичный стиль теста для cellfun и обнаружил, что это примерно в 3 раза медленнее, чем явный цикл. Опять же, этот результат противоположен чего я и ожидал.

мой вопрос: почему arrayfun и cellfun намного медленнее? И учитывая это, есть ли какие-либо веские причины использовать их (кроме того, чтобы код выглядел хорошо)?

Примечание: я говорю о стандартной версии arrayfun здесь, а не версия GPU из набора инструментов параллельной обработки.

EDIT: просто чтобы быть ясным, я знаю, что Func1 выше могут быть векторизованы, как указано Оли. Я выбрал его только потому, что он дает простой тест скорости для целей фактического вопроса.

EDIT: следуя предложению grungetta, я повторно сделал тест с feature accel off. Результаты таковы:

Elapsed time is 28.183422 seconds.
Elapsed time is 23.525251 seconds.

другими словами, Похоже, что большая часть разницы заключается в том, что ускоритель JIT делает гораздо лучшую работу по ускорению явного for петли, чем это делает arrayfun. Мне это кажется странным, так как arrayfun на самом деле предоставляет дополнительную информацию, т. е. ее использование показывает, что порядок вызовов Func1 не имеет значения. Кроме того, я отметил, что независимо от того, включен или выключен ускоритель JIT, моя система использует только один процессор...

2 96

2 ответа:

вы можете получить эту идею, запустив другие версии вашего кода. Рассмотрите возможность явного написания вычислений вместо использования функции в вашем цикле

tic
Soln3 = ones(T, N);
for t = 1:T
    for n = 1:N
        Soln3(t, n) = 3*x(t, n)^2 + 2*x(t, n) - 1;
    end
end
toc

время для вычисления на моем компьютере:

Soln1  1.158446 seconds.
Soln2  10.392475 seconds.
Soln3  0.239023 seconds.
Oli    0.010672 seconds.

теперь, хотя полностью "векторизованное" решение явно является самым быстрым, вы можете видеть, что определение функции, вызываемой для каждой записи x, является огромный накладные расходы. Просто явно выписывая вычисление, мы получили ускорение фактора 5. Я думаю, это показывает, что matlabs JIT compiler не поддерживает встроенные функции. Согласно ответу gnovice там, на самом деле лучше написать нормальную функцию, а не анонимную. Попробовать его.

следующий шаг-удалить (векторизовать) внутренний цикл:

tic
Soln4 = ones(T, N);
for t = 1:T
    Soln4(t, :) = 3*x(t, :).^2 + 2*x(t, :) - 1;
end
toc

Soln4  0.053926 seconds.

еще один фактор 5 ускорение: есть что-то в этих утверждениях, говорящих, что вы должны избегать циклов в MATLAB... Или это действительно так? Взгляните на это тогда

tic
Soln5 = ones(T, N);
for n = 1:N
    Soln5(:, n) = 3*x(:, n).^2 + 2*x(:, n) - 1;
end
toc

Soln5   0.013875 seconds.

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

теперь мы можем вернуться в Soln3. Порядок цикла там "по строкам". Давайте изменим его

tic
Soln6 = ones(T, N);
for n = 1:N
    for t = 1:T
        Soln6(t, n) = 3*x(t, n)^2 + 2*x(t, n) - 1;
    end
end
toc

Soln6  0.201661 seconds.

лучше, но все равно очень плохо. Одна петля-хорошо. Двойная петля - плохо. Я думаю, что MATLAB сделал некоторую достойную работу по улучшению производительности циклов, но все же цикл накладные расходы есть. Если бы у вас была какая-то более тяжелая работа внутри, вы бы не заметили. Но поскольку это вычисление ограничено полосой пропускания памяти, вы видите накладные расходы цикла. А ты будет еще более четко вижу накладные расходы на вызов Func1 там.

так что же с arrayfun? Никакой функции inlinig там тоже нет, так что много накладных расходов. Но почему так много хуже, чем двойной вложенный цикл? На самом деле, тема использования cellfun / arrayfun широко обсуждалась многими раз (например,здесь,здесь,здесь и здесь). Эти функции просто медленные, вы не можете использовать их для таких мелкозернистых вычислений. Вы можете использовать их для краткости кода и необычные переходы между ячейками и массивами. Но функция должна быть тяжелее, чем то, что вы написали:

tic
Soln7 = arrayfun(@(a)(3*x(:,a).^2 + 2*x(:,a) - 1), 1:N, 'UniformOutput', false);
toc

Soln7  0.016786 seconds.

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

так почему arrayfun медленнее, чем простая структура цикла? К сожалению, мы не можем сказать наверняка, так как нет исходного кода. Вы можете только догадываться, что поскольку arrayfun является функцией общего назначения, которая обрабатывает все виды различных структур данных и аргументов, это не обязательно очень быстро в простых случаях, которые вы можете напрямую выразить как гнезда цикла. Где есть ли накладные расходы, мы не можем знать. Можно ли избежать накладных расходов за счет лучшей реализации? А может и нет. Но, к сожалению, единственное, что мы можем сделать, это изучить производительность, чтобы определить случаи, в которых она работает хорошо, и те, где это не так.

обновление поскольку время выполнения этого теста короткое, чтобы получить надежные результаты, я добавил Теперь цикл вокруг тестов:

for i=1:1000
   % compute
end

несколько раз приведено ниже:

Soln5   8.192912 seconds.
Soln7  13.419675 seconds.
Oli     8.089113 seconds.

вы видите что arrayfun все еще плох, но по крайней мере не на три порядка хуже, чем векторизованное решение. С другой стороны, один цикл со столбцовыми вычислениями так же быстр, как и полностью векторизованная версия... Все это было сделано на одном процессоре. Результаты для Soln5 и Soln7 не меняются, если я переключусь на 2 ядра - в Soln5 мне придется использовать parfor, чтобы распараллелить его. Забудьте об ускорении... Soln7 не работает параллельно, потому что arrayfun не работает параллельно. Олис векторизован версия с другой стороны:

Oli  5.508085 seconds.

Это потому что!!!!

x = randn(T, N); 

- это не gpuarray тип;

все, что вам нужно сделать, это

x = randn(T, N,'gpuArray');