Ответ 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.