PLINQ хуже обычного LINQ

Подобно функции Explode (), предлагаемой Mef, но с несколькими отличиями (один из которых я считаю исправлением ошибок):

  type
    TArrayOfString = array of String;


  function SplitString(const aSeparator, aString: String; aMax: Integer = 0): TArrayOfString;
  var
    i, strt, cnt: Integer;
    sepLen: Integer;

    procedure AddString(aEnd: Integer = -1);
    var
      endPos: Integer;
    begin
      if (aEnd = -1) then
        endPos := i
      else
        endPos := aEnd + 1;

      if (strt < endPos) then
        result[cnt] := Copy(aString, strt, endPos - strt)
      else
        result[cnt] := '';

      Inc(cnt);
    end;

  begin
    if (aString = '') or (aMax < 0) then
    begin
      SetLength(result, 0);
      EXIT;
    end;

    if (aSeparator = '') then
    begin
      SetLength(result, 1);
      result[0] := aString;
      EXIT;
    end;

    sepLen := Length(aSeparator);
    SetLength(result, (Length(aString) div sepLen) + 1);

    i     := 1;
    strt  := i;
    cnt   := 0;
    while (i <= (Length(aString)- sepLen + 1)) do
    begin
      if (aString[i] = aSeparator[1]) then
        if (Copy(aString, i, sepLen) = aSeparator) then
        begin
          AddString;

          if (cnt = aMax) then
          begin
            SetLength(result, cnt);
            EXIT;
          end;

          Inc(i, sepLen - 1);
          strt := i + 1;
        end;

      Inc(i);
    end;

    AddString(Length(aString));

    SetLength(result, cnt);
  end;

Различия:

  1. Параметр aMax ограничивает количество возвращаемых строк
  2. Если входная строка заканчивается разделителем, то считается, что существует номинальная «пустая» конечная строка

Примеры:

SplitString(':', 'abc') returns      :    result[0]  = abc

SplitString(':', 'a:b:c:') returns   :    result[0]  = a
                                          result[1]  = b
                                          result[2]  = c
                                          result[3]  = <empty string>

SplitString(':', 'a:b:c:', 2) returns:    result[0]  = a
                                          result[1]  = b

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

Я также включил изменение распределения памяти, которое я предложил, с уточнением (я ошибочно предположил, что входная строка может содержать не более 50% разделителей, но вполне возможно, что она состоит из 100% разделительных строк, что дает массив пустых элементов!) [/ ​​g7]

13
задан Graviton 28 July 2010 в 15:46
поделиться

8 ответов

Первый: Прекратить использование DateTime для измерения времени выполнения. Вместо этого используйте секундомер. Код теста будет выглядеть так:

var watch = new Stopwatch();

var strList = Enumerable.Repeat(10, 10000000);

watch.Start();
var result = strList.Sum();
watch.Stop();

Console.WriteLine("Linear: {0}", watch.ElapsedMilliseconds);

watch.Reset();

watch.Start();
var parallelResult = strList.AsParallel().Sum();
watch.Stop();

Console.WriteLine("Parallel: {0}", watch.ElapsedMilliseconds);

Console.ReadKey();

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

Используя приведенный выше код, я вижу, что использование Sum () приводит к вызову ~ 95 мсек. Вызов .AsParallel (). Sum () возвращает около 185 мс.

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

21
ответ дан 1 December 2019 в 17:33
поделиться

Это классическая ошибка - думать: «Я проведу простой тест, чтобы сравнить производительность этого однопоточного кода с этим многопоточным кодом».

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

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


Рассмотрим эту аналогию.

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

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

неправильным способом сделать это было бы распараллеливать кладку кирпича - нанять еще около тысячи рабочих и поручить каждому рабочему укладывать по одному кирпичу за раз.Вы можете подумать: «Если один рабочий может уложить 2 кирпича в минуту, то тысяча рабочих сможет уложить 2000 кирпичей в минуту, так что я закончу эту работу в кратчайшие сроки!» Но реальность такова, что распараллеливая свою рабочую нагрузку на таком микроскопическом уровне, вы тратите огромное количество энергии, собирая и координируя всех своих сотрудников, поручив им задачи («положите этот кирпич прямо здесь»), следя за тем, чтобы никто не работа мешает работе других и т. д.

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


* По этой причине вы действительно можете довольно хорошо оценить выигрыш в производительности от распараллеливания в более трудоемком контексте, взяв любой быстро выполняющийся код и добавив Thread.Sleep (100) (или какое-то другое случайное число) до конца. Внезапно последовательное выполнение этого кода будет замедлено на 100 мс на итерацию, в то время как параллельное выполнение будет замедлено значительно меньше.

22
ответ дан 1 December 2019 в 17:33
поделиться

Другие указали на некоторые недостатки ваших тестов. Вот короткое консольное приложение для упрощения:

using System;
using System.Diagnostics;
using System.Linq;

public class Test
{
    const int Iterations = 1000000000;

    static void Main()
    {
        // Make sure everything's JITted
        Time(Sequential, 1);
        Time(Parallel, 1);
        Time(Parallel2, 1);
        // Now run the real tests
        Time(Sequential, Iterations);
        Time(Parallel,   Iterations);
        Time(Parallel2,  Iterations);
    }

    static void Time(Func<int, int> action, int count)
    {
        GC.Collect();
        Stopwatch sw = Stopwatch.StartNew();
        int check = action(count);
        if (count != check)
        {
            Console.WriteLine("Check for {0} failed!", action.Method.Name);
        }
        sw.Stop();
        Console.WriteLine("Time for {0} with count={1}: {2}ms",
                          action.Method.Name, count,
                          (long) sw.ElapsedMilliseconds);
    }

    static int Sequential(int count)
    {
        var strList = Enumerable.Repeat(1, count);
        return strList.Sum();
    }

    static int Parallel(int count)
    {
        var strList = Enumerable.Repeat(1, count);
        return strList.AsParallel().Sum();
    }

    static int Parallel2(int count)
    {
        var strList = ParallelEnumerable.Repeat(1, count);
        return strList.Sum();
    }
}

Компиляция:

csc /o+ /debug- Test.cs

Результаты на моем ноутбуке с четырехъядерным процессором i7; работает до 2 ядер быстрее или до 4 ядер медленнее. Обычно побеждает ParallelEnumerable.Repeat , за которым следует версия последовательности, за которой следует параллельное выполнение обычного Enumerable.Repeat .

Time for Sequential with count=1: 117ms
Time for Parallel with count=1: 181ms
Time for Parallel2 with count=1: 12ms
Time for Sequential with count=1000000000: 9152ms
Time for Parallel with count=1000000000: 44144ms
Time for Parallel2 with count=1000000000: 3154ms

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

8
ответ дан 1 December 2019 в 17:33
поделиться

Возможно, вы не учитываете время JIT? Вам следует запустить тест дважды и отбросить первый набор результатов.

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

var swatch = new Stopwatch();
swatch.StartNew();

var strList = Enumerable.Repeat(10, repeatedCount); 
var result = strList.AsParallel().Sum(); 

swatch.Stop();
textBox1.Text = swatch.Elapsed;

PLINQ добавляет некоторые накладные расходы на обработку последовательности. Но большая разница в вашем случае кажется чрезмерной. PLINQ имеет смысл, когда накладные расходы перевешиваются преимуществом запуска логики на нескольких ядрах / ЦП. Если у вас нет нескольких ядер, параллельная обработка не дает реальных преимуществ - и PLINQ должен обнаруживать такой случай и выполнять обработку последовательно.

РЕДАКТИРОВАТЬ: При создании встроенных тестов производительности такого типа вы должны убедиться, что вы не запускаете их под отладчиком или с включенным Intellitrace, так как это может значительно исказить время производительности.

1
ответ дан 1 December 2019 в 17:33
поделиться

Я бы рекомендовал использовать класс Stopwatch для метрик времени. В вашем случае это лучшая мера интервала.

0
ответ дан 1 December 2019 в 17:33
поделиться

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

0
ответ дан 1 December 2019 в 17:33
поделиться

Прочтите раздел «Побочные эффекты» этой статьи.

http://msdn.microsoft.com/en-us/magazine/cc163329.aspx

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

0
ответ дан 1 December 2019 в 17:33
поделиться

Комментарий Джастина по поводу накладных расходов совершенно правильный.

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

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

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

0
ответ дан 1 December 2019 в 17:33
поделиться
Другие вопросы по тегам:

Похожие вопросы: