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 в этом случае был медленнее.
Ответы
Ответ 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 упрощает параллельное программирование, но это не значит, что вы можете игнорировать мысль о детализации вашей работы.