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 выше может быть векторизован, как указал Оли. Я выбрал его только потому, что он дает простой тест скорости для целей фактического вопроса.

РЕДАКТИРОВАТЬ: Следуя предложению grungetta, я повторил тест с feature accel off. Результаты:

Elapsed time is 28.183422 seconds.
Elapsed time is 23.525251 seconds.
Другими словами, будет казаться, что большая часть разницы заключается в том, что ускоритель JIT делает гораздо лучшую работу по ускорению явного цикла for, чем при использовании arrayfun. Это кажется мне странным, так как arrayfun фактически предоставляет больше информации, т.е. Его использование показывает, что порядок вызовов Func1 не имеет значения. Кроме того, я отметил, что включение или выключение ускорителя JIT, моя система использует только один процессор...

Ответ 1

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

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 не поддерживает встроенные функции. Согласно ответу гновице, на самом деле лучше написать нормальную функцию, а не анонимную. Попробуйте.

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

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.

Ответ 2

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

x = randn(T, N); 

не является типом gpuarray;

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

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