Сравнительное тестирование независимых заданий на кластере

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

  • Покажите, как протестировать смеси в сравнении с эталоном последовательного кода и кода параллели задачи.

  • Объясните сильное и слабое масштабирование.

  • Обсудите некоторые потенциальные узкие места, и на клиенте и на кластере.

Примечание: Если при запуске этот пример на большом кластере, может потребоваться час, чтобы запуститься.

Связанные примеры:

Код, показанный в этом примере, может быть найден в этой функции:

function paralleldemo_distribjob_bench

Проверяйте кластерный профиль

Прежде чем мы будем взаимодействовать с кластером, мы проверяем, что клиент MATLAB® сконфигурирован согласно нашим потребностям. Вызов parcluster даст нам кластер с помощью профиля по умолчанию или выдаст ошибку, если значение по умолчанию не будет применимо.

myCluster = parcluster;

Синхронизация

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

Мы пишем все операции, чтобы быть максимально эффективными. Например, мы используем векторизованное создание задачи. Мы используем tic и toc для измерения прошедшего времени всех операций вместо того, чтобы использовать задание и свойства CreateTime задачиВремя начала, FinishTime, и т.д., потому что tic и toc дайте нам подвторую гранулярность. Обратите внимание на то, что мы также оснастили функцию задачи так, чтобы она возвратилась, время потратило выполнение наших расчетов сравнительного теста.

function [times, description] = timeJob(myCluster, numTasks, numHands)
    % The code that creates the job and its tasks executes sequentially in
    % the MATLAB client starts here.
    % We first measure how long it takes to create a job.
    timingStart = tic;
    start = tic;
    job = createJob(myCluster);
    times.jobCreateTime = toc(start);
    description.jobCreateTime = 'Job creation time';

    % Create all the tasks in one call to createTask, and measure how long
    % that takes.
    start = tic;
    taskArgs = repmat({{numHands, 1}}, numTasks, 1);
    createTask(job, @pctdemo_task_blackjack, 2, taskArgs);
    times.taskCreateTime = toc(start);
    description.taskCreateTime = 'Task creation time';

    % Measure how long it takes to submit the job to the cluster.
    start = tic;
    submit(job);
    times.submitTime = toc(start);
    description.submitTime = 'Job submission time';

    % Once the job has been submitted, we hope all its tasks execute in
    % parallel.  We measure how long it takes for all the tasks to start
    % and to run to completion.
    start = tic;
    wait(job);
    times.jobWaitTime = toc(start);
    description.jobWaitTime = 'Job wait time';

    % Tasks have now completed, so we are again executing sequential code
    % in the MATLAB client.  We measure how long it takes to retrieve all
    % the job results.
    start = tic;
    results = fetchOutputs(job);
    times.resultsTime = toc(start);
    description.resultsTime = 'Result retrieval time';

    % Verify that the job ran without any errors.
    if ~isempty([job.Tasks.Error])
        taskErrorMsgs = pctdemo_helper_getUniqueErrors(job);
        delete(job);
        error('pctexample:distribjobbench:JobErrored',  ...
          ['The following error(s) occurred during task ' ...
          'execution:\n\n%s'], taskErrorMsgs);
    end

    % Get the execution time of the tasks.  Our task function returns this
    % as its second output argument.
    times.exeTime = max([results{:,2}]);
    description.exeTime = 'Task execution time';

    % Measure how long it takes to delete the job and all its tasks.
    start = tic;
    delete(job);
    times.deleteTime = toc(start);
    description.deleteTime = 'Job deletion time';

    % Measure the total time elapsed from creating the job up to this
    % point.
    times.totalTime = toc(timingStart);
    description.totalTime = 'Total time';

    times.numTasks = numTasks;
    description.numTasks = 'Number of tasks';
end

Мы смотрим на некоторые детали того, что мы измеряем:

  • Время создания рабочих мест: время это берет, чтобы создать задание. Для кластера Планировщика Задания MATLAB это включает удаленный вызов, и Планировщик Задания MATLAB выделяет место в своей базе данных. Для других кластерных типов создание рабочих мест включает записывание нескольких файлов в диск.

  • Время создания задачи: время это берет, чтобы создать и сохранить информацию задачи. Планировщик Задания MATLAB сохраняет это в своей базе данных, тогда как другие кластерные типы сохраняют его в файлах в файловой системе.

  • Время представления задания: время это берет, чтобы представить задание. Для кластера Планировщика Задания MATLAB мы говорим ему начинать выполнять задание, которое это имеет в его базе данных. Мы просим, чтобы другие кластерные типы выполнили все задачи, которые мы создали.

  • Время ожидания задания: время мы ожидаем после представления задания до завершения задания. Это включает все действия, которые происходят между представлением задания и когда задание завершилось, такие как: кластер, возможно, должен запустить всех рабочих и отправить рабочим информацию о задаче; рабочие считывают информации задачи и выполняют функцию задачи. В случае кластера Планировщика Задания MATLAB рабочие затем отправляют результаты задачи в Планировщик Задания MATLAB, который пишет им в его базу данных, тогда как для других кластерных типов, рабочие пишут результаты задачи в диск.

  • Время выполнения задачи: время, проведенное, симулируя блэк джек. Мы оснащаем функцию задачи, чтобы точно измериться на этот раз. Это время также включено во время ожидания задания.

  • Время поиска результатов: время это берет, чтобы принести результаты задания в клиент MATLAB. Для Планировщика Задания MATLAB мы получаем их из его базы данных. Для других кластерных типов мы читаем их из файловой системы.

  • Время удаления Джоба: время это берет, чтобы удалить все задание и информацию о задаче. MATLAB Джоб Шедулер удаляет его из своей базы данных. Для других кластерных типов мы удаляем файлы из файловой системы.

  • Общее время: время это берет, чтобы выполнить все вышеупомянутое.

Выбор проблемного размера

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

numHands = 1.2e6;
numReps = 5;

Мы исследуем ускорение путем работы различного количества рабочих, начиная с 1, 2, 4, 8, 16, и т.д., и окончания столькими рабочими, сколько мы можем возможно использовать. В этом примере мы принимаем, что выделили доступ к кластеру для сравнительного тестирования, и что NumWorkers кластера свойство было установлено правильно. Предположение, что, чтобы иметь место, каждая задача выполнится сразу же на преданном рабочем, таким образом, мы сможем приравнять количество задач, которые мы представляем с количеством рабочих, которые выполняют их.

numWorkers = myCluster.NumWorkers ;
if isinf(numWorkers) || (numWorkers == 0)
    error('pctexample:distribjobbench:InvalidNumWorkers', ...
          ['Cannot deduce the number of workers from the cluster.  ' ...
           'Set the NumWorkers on your default profile to be ' ...
           'a value other than 0 or inf.']);
end

numTasks = [pow2(0:ceil(log2(numWorkers) - 1)), numWorkers];

Слабые измерения масштабирования

Мы варьируемся количество задач в задании и имеем каждую задачу, выполняют установленную сумму работы. Это называется слабым масштабированием и - то, о чем мы действительно заботимся больше всего, потому что мы обычно масштабируем до кластера, чтобы решить большие задачи. Это должно быть по сравнению с сильными сравнительными тестами масштабирования, показанными позже в этом примере. Ускорение на основе слабого масштабирования также известно как масштабированное ускорение.

fprintf(['Starting weak scaling timing.  ' ...
         'Submitting a total of %d jobs.\n'], numReps*length(numTasks));
for j = 1:length(numTasks)
    n = numTasks(j);
    for itr = 1:numReps
        [rep(itr), description] = timeJob(myCluster, n, numHands);  %#ok<AGROW>
    end
    % Retain the iteration with the lowest total time.
    totalTime = [rep.totalTime];
    fastest = find(totalTime == min(totalTime), 1);
    weak(j) = rep(fastest);  %#ok<AGROW>
    fprintf('Job wait time with %d task(s): %f seconds\n', ...
            n, weak(j).jobWaitTime);
end
Starting weak scaling timing.  Submitting a total of 45 jobs.
Job wait time with 1 task(s): 59.631733 seconds
Job wait time with 2 task(s): 60.717059 seconds
Job wait time with 4 task(s): 61.343568 seconds
Job wait time with 8 task(s): 60.759119 seconds
Job wait time with 16 task(s): 63.016560 seconds
Job wait time with 32 task(s): 64.615484 seconds
Job wait time with 64 task(s): 66.581806 seconds
Job wait time with 128 task(s): 91.043285 seconds
Job wait time with 256 task(s): 150.411704 seconds

Последовательное выполнение

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

seqTime = inf;
for itr = 1:numReps
    start = tic;
    pctdemo_task_blackjack(numHands, 1);
    seqTime = min(seqTime, toc(start));
end
fprintf('Sequential execution time: %f seconds\n', seqTime);
Sequential execution time: 84.771630 seconds

Ускорение на основе слабого масштабирования и общее время выполнения

Мы сначала смотрим на полное ускорение, достигнутое путем работы различных количеств рабочих. Ускорение основано на общем времени, используемом в расчетах, таким образом, это включает и последовательное и параллельные фрагменты нашего кода.

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

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

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

titleStr = sprintf(['Speedup based on total execution time\n' ...
                    'Note: This graph does not identify performance ' ...
                    'bottlenecks']);
pctdemo_plot_distribjob('speedup', [weak.numTasks], [weak.totalTime], ...
    weak(1).totalTime, titleStr);

Подробные графики, часть 1

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

  • Создание задачи

  • Извлечение выходных аргументов задания

  • Время разрушения задания

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

  • Время создания рабочих мест

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

pctdemo_plot_distribjob('fields', weak, description, ...
    {'jobCreateTime', 'taskCreateTime',  'resultsTime', 'deleteTime'}, ...
    'Time in seconds');

Нормированные времена

Мы уже пришли к заключению, что время создания задачи, как ожидают, увеличится, в то время как мы увеличиваем число задач, как делает время, чтобы получить выходные аргументы задания и удалить задание. Однако это увеличение - то, вследствие того, что мы выполняем больше, работают, когда мы увеличиваем число рабочих/задач. Это поэтому значимо, чтобы измерить КПД этих трех действий путем взгляда в то время, когда это берет, чтобы выполнить эти операции и нормировать его количеством задач. Таким образом, мы можем надеяться видеть, остается ли какой-либо из следующих раз постоянным, увеличение или уменьшение, когда мы варьируемся количество задач:

  • Время это берет, чтобы создать одну задачу

  • Время это берет, чтобы получить выходные аргументы от одной задачи

  • Время это берет, чтобы удалить задачу в задании

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

pctdemo_plot_distribjob('normalizedFields', weak, description, ...
    {'taskCreateTime', 'resultsTime', 'deleteTime'});

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

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

Подробные графики, часть 2

Возможно, что время, проведенное в каждом из следующих шагов, меняется в зависимости от количества задач, но мы надеемся, что это не делает:

  • Время представления задания.

  • Время выполнения задачи. Это получает время, проведенное, симулируя блэк джек. Ничто больше, ничто меньше.

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

pctdemo_plot_distribjob('fields', weak, description, ...
    {'submitTime', 'exeTime'});

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

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

  • График времени выполнения задачи наиболее вероятен отсоединить конкуренция ресурса и аппаратные ограничения. Например, время выполнения задачи могло увеличиться, если мы выполняем несколько рабочих на том же компьютере, из-за конкуренции для пропускной способности ограниченной памяти. Другой пример конкуренции ресурса - то, если функция задачи должна была считать или записать большие файлы данных с помощью одной, совместно используемой файловой системы. Функция задачи в этом примере, однако, не получает доступ к файловой системе вообще. Эти типы аппаратных ограничений покрыты очень подробно в Конкуренции Ресурса в качестве примера в проблемах Параллели Задачи.

Ускорение на основе слабого времени ожидания масштабирования и задания

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

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

titleStr = 'Speedup based on job wait time compared to one task';
pctdemo_plot_distribjob('speedup', [weak.numTasks], [weak.jobWaitTime], ...
    weak(1).jobWaitTime, titleStr);

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

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

titleStr = 'Speedup based on job wait time compared to sequential time';
pctdemo_plot_distribjob('speedup', [weak.numTasks], [weak.jobWaitTime], ...
    seqTime, titleStr);

Сравнение времени ожидания задания со временем выполнения задачи

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

titleStr = 'Difference between job wait time and task execution time';
pctdemo_plot_distribjob('barTime', [weak.numTasks], ...
    [weak.jobWaitTime] - [weak.exeTime], titleStr);

Сильные измерения масштабирования

Мы теперь измеряем время выполнения проблемы фиксированного размера при варьировании количества рабочих, которых мы используем, чтобы решить задачу. Это называется сильным масштабированием, и известно, что, если приложение имеет какие-либо последовательные части, существует верхний предел ускорению, которое может быть достигнуто с сильным масштабированием. Это формализовано в законе Амдаля, который был широко обсужден и обсужден за эти годы.

Можно легко столкнуться с пределами ускорения с сильным масштабированием при представлении заданий кластеру. Если выполнение задачи будет иметь фиксированные издержки (который оно обычно делает), даже если это будет всего одна секунда, время выполнения нашего приложения никогда не будет понижаться одна секунда. В нашем случае мы начинаем с приложения, которое выполняется приблизительно за 60 секунд на одном работнике MATLAB. Если мы делим расчеты между 60 рабочими, может потребоваться всего одна секунда для каждого рабочего, чтобы вычислить ее фрагмент полной проблемы. Однако гипотетическое выполнение задачи наверху одной секунды стало основным фактором полного времени выполнения.

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

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

fprintf(['Starting strong scaling timing.  ' ...
         'Submitting a total of %d jobs.\n'], numReps*length(numTasks))
for j = 1:length(numTasks)
    n = numTasks(j);
    strongNumHands = ceil(numHands/n);
    for itr = 1:numReps
        rep(itr) = timeJob(myCluster, n, strongNumHands);
    end
    ind = find([rep.totalTime] == min([rep.totalTime]), 1);
    strong(n) = rep(ind); %#ok<AGROW>
    fprintf('Job wait time with %d task(s): %f seconds\n', ...
            n, strong(n).jobWaitTime);
end
Starting strong scaling timing.  Submitting a total of 45 jobs.
Job wait time with 1 task(s): 60.531446 seconds
Job wait time with 2 task(s): 31.745135 seconds
Job wait time with 4 task(s): 18.367432 seconds
Job wait time with 8 task(s): 11.172390 seconds
Job wait time with 16 task(s): 8.155608 seconds
Job wait time with 32 task(s): 6.298422 seconds
Job wait time with 64 task(s): 5.253394 seconds
Job wait time with 128 task(s): 5.302715 seconds
Job wait time with 256 task(s): 49.428909 seconds

Ускорение на основе сильного масштабирования и общее время выполнения

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

titleStr = sprintf(['Speedup based on total execution time\n' ...
                    'Note: This graph does not identify performance ' ...
                    'bottlenecks']);
pctdemo_plot_distribjob('speedup', [strong.numTasks], ...
    [strong.totalTime].*[strong.numTasks], strong(1).totalTime, titleStr);

Альтернатива для коротких задач: Parfor

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

pool = parpool(numWorkers);
parforTime = inf;
strongNumHands = ceil(numHands/numWorkers);
for itr = 1:numReps
    start = tic;
    r = cell(1, numWorkers);
    parfor i = 1:numWorkers
        r{i} = pctdemo_task_blackjack(strongNumHands, 1); %#ok<PFOUS>
    end
    parforTime = min(parforTime, toc(start));
end
delete(pool);
Starting parallel pool (parpool) using the 'bigMJS' profile ... connected to 256 workers.
Analyzing and transferring files to the workers ...done.

Ускорение на основе сильного масштабирования с PARFOR

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

fprintf('Execution time with parfor using %d workers: %f seconds\n', ...
    numWorkers, parforTime);
fprintf(['Speedup based on strong scaling with parfor using ', ...
    '%d workers: %f\n'], numWorkers, seqTime/parforTime);
Execution time with parfor using 256 workers: 1.126914 seconds
Speedup based on strong scaling with parfor using 256 workers: 75.224557

Сводные данные

Мы видели различие между слабым и сильным масштабированием и обсудили, почему мы предпочитаем смотреть на слабое масштабирование: Это измеряет нашу способность решить большие задачи на кластере (больше симуляций, больше итераций, больше данных, и т.д.). Большое количество графиков и сумма детали в этом примере должны также быть завещанием к тому, что сравнительные тесты не могут быть сведены к одному номеру или одному графику. Мы должны посмотреть на целое изображение, чтобы понять, может ли производительность приложения быть приписана приложению, кластерному оборудованию или программному обеспечению или комбинации обоих.

Мы также видели это для коротких вычислений, parfor может быть большая альтернатива заданиям и задачам. Для большего количества результатов сравнительного тестирования с помощью parfor, смотрите пример Простое Сравнительное тестирование PARFOR Используя Блэк джек.

end
Для просмотра документации необходимо авторизоваться на сайте