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

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

13 голосов
/ 28 июля 2010

Удивительно, но использование PLINQ не принесло пользы в небольшом тестовом примере, который я создал;на самом деле, он был даже хуже, чем обычный LINQ.

Вот код теста:

    int repeatedCount = 10000000;
    private void button1_Click(object sender, EventArgs e)
    {
        var currTime = DateTime.Now;
        var strList = Enumerable.Repeat(10, repeatedCount);
        var result = strList.AsParallel().Sum();

        var currTime2 = DateTime.Now;
        textBox1.Text = (currTime2.Ticks-currTime.Ticks).ToString();

    }

    private void button2_Click(object sender, EventArgs e)
    {
        var currTime = DateTime.Now;
        var strList = Enumerable.Repeat(10, repeatedCount);
        var result = strList.Sum();

        var currTime2 = DateTime.Now;
        textBox2.Text = (currTime2.Ticks - currTime.Ticks).ToString();
    }

Результат?

textbox1: 3437500
textbox2: 781250

Итак, LINQ отнимает меньше временичем PLINQ для завершения аналогичной операции!

Что я делаю не так?Или есть какой-то поворот, о котором я не знаю?

Редактировать: я обновил свой код, чтобы использовать секундомер, и все же, то же самое поведение сохранялось.Чтобы обесценить эффект JIT, я несколько раз пытался щелкнуть по button1 и button2 в произвольном порядке.Хотя время, которое я получил, могло быть другим, но качественное поведение оставалось: PLINQ действительно был медленнее в этом случае.

Ответы [ 9 ]

22 голосов
/ 28 июля 2010

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

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

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


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

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

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

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

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


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

21 голосов
/ 28 июля 2010

Первый: Прекратить использование 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.

8 голосов
/ 28 июля 2010

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

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

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

1 голос
/ 11 февраля 2016

Что-то более важное, о чем я не упомянул, это то, что .AsParallel будет иметь разную производительность в зависимости от используемой коллекции.

В моих тестах PLINQ быстрее , чем LINQ, когда НЕ используется в IEnumerable (Enumerable.Repeat):

  29ms  PLINQ  ParralelQuery    
  30ms   LINQ  ParralelQuery    
  30ms  PLINQ  Array
  38ms  PLINQ  List    
 163ms   LINQ  IEnumerable
 211ms   LINQ  Array
 213ms   LINQ  List
 273ms  PLINQ  IEnumerable
4 processors

Код в VB, но он показывает, что использование .ToArray сделало версию PLINQ в несколько раз быстрее

    Dim test = Function(LINQ As Action, PLINQ As Action, type As String)
                   Dim sw1 = Stopwatch.StartNew : LINQ() : Dim ts1 = sw1.ElapsedMilliseconds
                   Dim sw2 = Stopwatch.StartNew : PLINQ() : Dim ts2 = sw2.ElapsedMilliseconds
                   Return {String.Format("{0,4}ms   LINQ  {1}", ts1, type), String.Format("{0,4}ms  PLINQ  {1}", ts2, type)}
               End Function

    Dim results = New List(Of String) From {Environment.ProcessorCount & " processors"}
    Dim count = 12345678, iList = Enumerable.Repeat(1, count)

    With iList : results.AddRange(test(Sub() .Sum(), Sub() .AsParallel.Sum(), "IEnumerable")) : End With
    With iList.ToArray : results.AddRange(test(Sub() .Sum(), Sub() .AsParallel.Sum(), "Array")) : End With
    With iList.ToList : results.AddRange(test(Sub() .Sum(), Sub() .AsParallel.Sum(), "List")) : End With
    With ParallelEnumerable.Repeat(1, count) : results.AddRange(test(Sub() .Sum(), Sub() .AsParallel.Sum(), "ParralelQuery")) : End With

    MessageBox.Show(String.join(Environment.NewLine, From l In results Order By l))

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

1 голос
/ 28 июля 2010

Возможно ли, что вы не учитываете время 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, поскольку это может существенно искажать временные характеристики производительности.

0 голосов
/ 28 июля 2010

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

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

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

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

0 голосов
/ 28 июля 2010

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

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

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

0 голосов
/ 28 июля 2010

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

0 голосов
/ 28 июля 2010

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

...