Подтвердить что ты не робот

PLINQ выполняет хуже, чем обычный LINQ

Удивительно, но использование 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 в этом случае был медленнее.

4b9b3361

Ответ 1

Сначала: Остановить использование 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();

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

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

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

Ответ 2

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

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

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


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

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

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

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

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


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

Ответ 3

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

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

public class Test
{
    const int Iterations = 1000000000;

    static void Main()
    {
        // Make sure everything 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

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

Ответ 4

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

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

Ответ 5

Что-то более важное, о чем я не упоминал, это то, что .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))

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

Ответ 6

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

Ответ 7

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

Ответ 8

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

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

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

Ответ 9

Джастин комментирует накладные расходы точно.

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

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

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