Сохранение времени и памяти с помощью parfor?

Рассмотрим prova.mat в MATLAB, полученном следующим образом:

for w=1:100
    for p=1:9    
        A{p}=randn(100,1); 
    end
    baseA_.A=A;

    eval(['baseA.A' num2str(w) '= baseA_;'])

end

save(sprintf('prova.mat'),'-v7.3', 'baseA')

Чтобы иметь представление о фактических размерах в моих данных, 1x9 cell в A1 состоит из следующих массивов 9: 904x5, 913x5, 1722x5, 4136x5, 9180x5, 3174x5, 5970x5, 4455x5, 340068x5. Другие Aj имеют сходный состав.

Рассмотрим следующий код

clear all
load prova
tic
parfor w=1:100
       indA=sprintf('A%d', w);
       Aarr=baseA.(indA).A;
       Boot=[];
       for p=1:9
           C=randn(100,1).*Aarr{p};
           Boot=[Boot; C];  
       end
       D{w}=Boot;
end
toc

Если я запустил цикл parfor с местными работниками 4 в моем Macbook Pro, он занимает 1,2 секунды. Заменяя parfor на for, требуется 0,01 сек.

С моими фактическими данными разница времени составляет 31 сек против 7 сек [создание матрицы C также более сложное].

Если вы правильно поняли, проблема в том, что компьютер должен отправлять baseA каждому локальному рабочему, и для этого требуется время и память.

Не могли бы вы предложить решение, способное сделать parfor более удобным, чем for? Я думал, что сохранение всех ячеек в baseA было способом сэкономить время, загрузив один раз в начале, но, возможно, я ошибаюсь.

Ответ 1

Основная информация

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

Когда не используется одна из неявно многопоточных функций, parfor в основном рекомендуется в двух случаях: много итераций в цикле (например, как 1e10), или если каждая итерация занимает очень много времени (например, eig(magic(1e4))), Во втором случае вы можете рассмотреть возможность использования spmd (медленнее, чем parfor в моем опыте). Причина parfor медленнее, чем for цикла для коротких диапазонов или быстрых итераций накладных расходов, необходимых для управления всех работников правильно, а не просто делать расчет.

Проверьте этот вопрос для получения информации о разделении данных между отдельными работниками.

Бенчмаркинг

Код

Рассмотрим следующий пример, чтобы увидеть поведение for а не parfor. Сначала откройте параллельный пул, если вы еще этого не сделали:

gcp; % Opens a parallel pool using your current settings

Затем выполните пару больших циклов:

n = 1000; % Iteration number
EigenValues = cell(n,1); % Prepare to store the data
Time = zeros(n,1);
for ii = 1:n
tic
    EigenValues{ii,1} = eig(magic(1e3)); % Might want to lower the magic if it takes too long
Time(ii,1) = toc; % Collect time after each iteration
end

figure; % Create a plot of results
plot(1:n,t)
title 'Time per iteration'
ylabel 'Time [s]'
xlabel 'Iteration number[-]';

Затем сделайте то же самое с parfor вместо for. Вы заметите, что среднее время за итерацию увеличивается (от 0,27 до 0,39 с для моего случая). Однако осознайте, что parfor использовал всех доступных рабочих, поэтому общее время (sum(Time)) должно быть разделено на количество ядер в вашем компьютере. Так что для моего случая общее время сократилось с 270 до 49 с, так как у меня есть процессор октакор.

Таким образом, хотя время выполнения каждой отдельной итерации увеличивается с использованием parfor относительно использования for, общее время значительно сокращается.

Результаты

parforbenchtest

На этом рисунке показаны результаты теста, так как я только что провел его на своем домашнем ПК. Я использовал n=1000 и eig(500); мой компьютер имеет процессор I5-750 с частотой 2,66 ГГц с четырьмя ядрами и работает под управлением MATLAB R2012a. Как вы можете видеть, среднее значение параллельного теста колеблется около 0,29 с большим разбросом, в то время как последовательный код довольно стабилен около 0,24 с. Общее время, однако, сократилось с 234 до 72 секунд, что в 3,25 раза больше. Причиной того, что это не совсем 4, являются накладные расходы памяти, выраженные в дополнительном времени, которое занимает каждая итерация. Перегрузка памяти связана с тем, что MATLAB необходимо проверить, что делает каждое ядро, и убедиться, что каждая итерация цикла выполняется только один раз и что данные помещаются в правильное место хранения.

Ответ 2

Сетчатые данные передаются в массив ячеек

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

Упрощенный пример такого data следующий: первый столбец содержит переменную группировки:

ngroups = 1000;
nrows   = 1e6;
data    = [randi(ngroups,[nrows,1]), randn(nrows,1)];
data(1:5,:)
ans =
          620     -0.10696
          586      -1.1771
          625       2.2021
          858      0.86064
           78       1.7456

Теперь для простоты предположим, что меня интересует sum() по группе значений во втором столбце. Я могу перебирать по группам, индексировать интересующие элементы и суммировать их. Я буду выполнять эту задачу с помощью цикла for, простого parfor и parfor с данными нарезанных, и будет сравнивать тайминги.

Имейте в виду, что это игрушечный пример, и меня не интересуют альтернативные решения, такие как bsxfun(), это не точка анализа.

Результаты

Заимствуя один и тот же тип сюжета из Adriaan, я сначала подтверждаю те же самые выводы о простой parfor vs for. Во-вторых, оба метода полностью превосходят parfor на срезанных данных, которые занимают чуть больше 2 секунд для заполнения набора данных с 10 миллионами строк (операция нарезки включена в синхронизацию). Простой parfor занимает 24 секунды, а for - почти в два раза больше времени (я на Win7 64, R2016a и i5-3570 с 4 ядрами).

введите описание изображения здесь

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

  • накладные расходы от всех данных, передаваемых рабочим,
  • операции индексирования в постоянно растущие наборы данных.

Код

ngroups = 1000;
nrows   = 1e7;
data    = [randi(ngroups,[nrows,1]), randn(nrows,1)];

% Simple for
[out,t] = deal(NaN(ngroups,1));
overall = tic;
for ii = 1:ngroups
    tic
    idx     = data(:,1) == ii;
    out(ii) = sum(data(idx,2));
    t(ii)   = toc;
end
s.OverallFor = toc(overall);
s.TimeFor    = t;
s.OutFor     = out;

% Parfor
try parpool(4); catch, end
[out,t] = deal(NaN(ngroups,1));
overall = tic;
parfor ii = 1:ngroups
    tic
    idx     = data(:,1) == ii;
    out(ii) = sum(data(idx,2));
    t(ii)   = toc;
end
s.OverallParfor = toc(overall);
s.TimeParfor    = t;
s.OutParfor     = out;

% Sliced parfor
[out,t] = deal(NaN(ngroups,1));
overall = tic;
c       = cache2cell(data,data(:,1));
s.TimeDataSlicing = toc(overall);
parfor ii = 1:ngroups
    tic
    out(ii) = sum(c{ii}(:,2));
    t(ii)   = toc;
end
s.OverallParforSliced = toc(overall);
s.TimeParforSliced    = t;
s.OutParforSliced     = out;

x = 1:ngroups;
h = plot(x, s.TimeFor,'xb',x,s.TimeParfor,'+r',x,s.TimeParforSliced,'.g');
set(h,'MarkerSize',1)
title 'Time per iteration'
ylabel 'Time [s]'
xlabel 'Iteration number[-]';
legend({sprintf('for          : %5.2fs',s.OverallFor),...
        sprintf('parfor       : %5.2fs',s.OverallParfor),...
        sprintf('parfor_sliced: %5.2fs',s.OverallParforSliced)},...
        'interpreter', 'none','fontname','courier')

Вы можете найти cache2cell() на моем github repo.

Простой для нарезанных данных

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

введите описание изображения здесь

Однако это пример игрушки, где стоимость внутреннего цикла полностью выполняется операцией индексирования. Следовательно, для того, чтобы parfor стоило, внутренний цикл должен быть более сложным и/или распространяться.

Сохранение памяти с нарезанным парром

Теперь, если предположить, что ваш внутренний цикл более сложный, а простой цикл for медленнее, давайте посмотрим, сколько памяти мы сохраняем, избегая передачи данных в parfor с 4 рабочими и набором данных с 50 миллионами строк (для около 760 МБ в ОЗУ).

введите описание изображения здесь

Как вы можете видеть, почти 3 ГБ дополнительной памяти отправляются рабочим. Операция среза требует некоторой памяти, которая должна быть завершена, но все же намного меньше, чем операция широковещания, и в принципе может перезаписать исходный набор данных, что приведет к незначительной стоимости ОЗУ после завершения. Наконец, parfor на нарезанных данных будет использовать только небольшую долю памяти, то есть такую ​​сумму, которая соответствует используемым срезам.

Нарезанный в ячейку

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

Пока наш образец data выглядел так:

data(1:5,:)
ans =
          620     -0.10696
          586      -1.1771
          625       2.2021
          858      0.86064
           78       1.7456

out sliced ​​ c выглядит как

c(1:5)
ans = 
    [ 969x2 double]
    [ 970x2 double]
    [ 949x2 double]
    [ 986x2 double]
    [1013x2 double]

где c{1} -

c{1}(1:5,:)
ans =
            1      0.58205
            1      0.80183
            1     -0.73783
            1      0.79723
            1       1.0414