Самым быстрым способом сделать это, вероятно, будет ручное вращение петли Parallel.ForEach()
.
Plinq
может даже не дать вам ускорение по сравнению с однопоточным подходом, и, конечно, оно не будет таким быстрым, как Parallel.ForEach()
.
Вот пример кода синхронизации. Когда вы попробуете это, убедитесь, что это сборка RELEASE и вы не запускаете ее под отладчиком (который отключит оптимизатор JIT, даже если это сборка RELEASE):
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Demo
{
static class Program
{
static void Main()
{
// Create some random bytes (using a seed to ensure it's the same bytes each time).
var rng = new Random(12345);
byte[] byteArr = new byte[500_000_000];
rng.NextBytes(byteArr);
// Time single-threaded Linq.
var sw = Stopwatch.StartNew();
long sum = byteArr.Sum(x => (long)x);
Console.WriteLine($"Single-threaded Linq took {sw.Elapsed} to calculate sum as {sum}");
// Time single-threaded loop;
sw.Restart();
sum = 0;
foreach (var n in byteArr)
sum += n;
Console.WriteLine($"Single-threaded took {sw.Elapsed} to calculate sum as {sum}");
// Time Plinq
sw.Restart();
sum = byteArr.AsParallel().Sum(x => (long)x);
Console.WriteLine($"Plinq took {sw.Elapsed} to calculate sum as {sum}");
// Time Parallel.ForEach() with partitioner.
sw.Restart();
sum = 0;
Parallel.ForEach
(
Partitioner.Create(0, byteArr.Length),
() => 0L,
(subRange, loopState, threadLocalState) =>
{
for (int i = subRange.Item1; i < subRange.Item2; i++)
threadLocalState += byteArr[i];
return threadLocalState;
},
finalThreadLocalState =>
{
Interlocked.Add(ref sum, finalThreadLocalState);
}
);
Console.WriteLine($"Parallel.ForEach with partioner took {sw.Elapsed} to calculate sum as {sum}");
}
}
}
Результаты, полученные при сборке x64 на моем octo-core PC:
- Однопоточному Linq потребовалось 00: 00: 03.1160235, чтобы вычислить сумму как 63748717461
- Для однопоточной обработки потребовалось 00: 00: 00.7596687, чтобы вычислить сумму как 63748717461
- Plinq потребовалось 00: 00: 01.0305913, чтобы вычислить сумму как 63748717461
- Parallel.ForEach с разделителем заняло 00: 00: 00.0839141, чтобы вычислить сумму как 63748717461
Результаты, полученные при сборке x86:
- Однопоточному Linq потребовалось 00: 00: 02.6964067, чтобы вычислить сумму как 63748717461
- Однопоточное заняло 00: 00: 00.8200462 для вычисления суммы как 63748717461
- Plinq потребовалось 00: 00: 01.1251899, чтобы вычислить сумму как 63748717461
- Parallel.ForEach с разделителем заняло 00: 00: 00.1084805, чтобы вычислить сумму как 63748717461
Как вы можете видеть, Parallel.ForEach()
со сборкой x64 является самым быстрым (вероятно, потому, что он вычисляет всего long
, а не из-за большего адресного пространства).
* * * * * * * * * * * * * * * * * * * *
примерно в три раза быстрее, чем решение для многопоточных систем Linq. *1040*
Parallel.ForEach()
с разделителем работает более чем в 30 раз.
Но примечательно, что однопоточный код не linq работает быстрее, чем код Plinq
. В этом случае использование Plinq
бессмысленно; это делает вещи медленнее!
Это говорит нам о том, что ускорение связано не только с многопоточностью, но и с накладными расходами Linq
и Plinq
по сравнению с ручным циклом.
Вообще говоря, вы должны использовать Plinq
, только если обработка каждого элемента занимает относительно много времени (а добавление байта к промежуточному итогу занимает очень короткое время).
Преимущество Plinq перед Parallel.ForEach()
с разделителем состоит в том, что писать на намного проще - однако, если он оказывается медленнее, чем простой цикл foreach
, его полезность сомнительна. Поэтому выбор времени перед выбором решения очень важен!