Производительность параллелизма .NET на стороне клиента
Я пишу клиентское приложение .NET, которое, как ожидается, будет использовать много потоков. Меня предупреждали, что производительность .NET очень плоха, когда речь заходит о параллелизме. Хотя я не пишу приложение реального времени, я хочу убедиться, что мое приложение масштабируемо (т. е. позволяет много потоков) и каким-то образом сопоставимо с эквивалентным приложением C++.
Каков ваш опыт? Что такое релевантный ориентир?
5 ответов:
Я собрал быстрый и грязный бенчмарк в C#, используя в качестве теста генератор простых чисел. Тест генерирует простые числа с постоянным пределом (я выбрал 500000), используя простое сито реализации Eratosthenes, и повторяет тест 800 раз, распараллеленный по определенному числу потоков, либо используя .NET
Тест был выполнен на четырехъядерном процессоре Q6600 под управлением Windows Vista (x64). Это не использование параллельной библиотеки задач, а просто простые потоки. Это было выполните следующие сценарии:ThreadPool
, либо автономные потоки.
- последовательное выполнение (без резьбы)
- 4 потока (то есть по одному на ядро), используя
ThreadPool
- 40 потоков, использующих
ThreadPool
(для проверки эффективности самого пула)- 4 автономных потока
- 40 автономных потоков, имитирующих давление переключения контекста
Результаты были следующими:
Test | Threads | ThreadPool | Time -----+---------+------------+-------- 1 | 1 | False | 00:00:17.9508817 2 | 4 | True | 00:00:05.1382026 3 | 40 | True | 00:00:05.3699521 4 | 4 | False | 00:00:05.2591492 5 | 40 | False | 00:00:05.0976274
Из этого можно сделать следующие выводы:
Распараллеливание не идеально (как и ожидалось - это никогда не происходит, независимо от окружающей среды), но разделение нагрузки на 4 ядра приводит к увеличению пропускной способности примерно в 3,5 раза, на что вряд ли стоит жаловаться.
Была незначительная разница между 4 и 40 потоками, использующими
ThreadPool
, Что означает, что никаких значительных расходов не происходит с пулом, даже когда вы бомбардируете его запросами.Была незначительная разница между
ThreadPool
и свободнопоточными версиями, что означает, чтоThreadPool
не имеет каких-либо существенных "постоянных" расходов;Была незначительная разница между 4-потоковыми и 40-потоковыми свободнопоточными версиями, что означает, что .NET работает не хуже, чем можно было бы ожидать при интенсивном переключении контекста.
Нужен ли нам вообще бенчмарк C++ для сравнения? Результаты довольно ясны: потоки в .NET не являются медленными. Если толькоВы , программист, не напишете плохой многопоточный код и не получите истощение ресурсов или блокирование конвоев, вам действительно не нужно беспокоиться.
С .NET 4.0 и TPL и улучшениями
ThreadPool
, очередями воровства работы и всем этим классным материалом, у вас есть еще больше свободы для написания "сомнительного" кода и все еще эффективно его запускать. Вы вообще не получаете эти функции от C++.Для справки, вот тестовый код:
using System; using System.Collections.Generic; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Threading; namespace ThreadingTest { class Program { private static int PrimeMax = 500000; private static int TestRunCount = 800; static void Main(string[] args) { Console.WriteLine("Test | Threads | ThreadPool | Time"); Console.WriteLine("-----+---------+------------+--------"); RunTest(1, 1, false); RunTest(2, 4, true); RunTest(3, 40, true); RunTest(4, 4, false); RunTest(5, 40, false); Console.WriteLine("Done!"); Console.ReadLine(); } static void RunTest(int sequence, int threadCount, bool useThreadPool) { TimeSpan duration = Time(() => GeneratePrimes(threadCount, useThreadPool)); Console.WriteLine("{0} | {1} | {2} | {3}", sequence.ToString().PadRight(4), threadCount.ToString().PadRight(7), useThreadPool.ToString().PadRight(10), duration); } static TimeSpan Time(Action action) { Stopwatch sw = new Stopwatch(); sw.Start(); action(); sw.Stop(); return sw.Elapsed; } static void GeneratePrimes(int threadCount, bool useThreadPool) { if (threadCount == 1) { TestPrimes(TestRunCount); return; } int testsPerThread = TestRunCount / threadCount; int remaining = threadCount; using (ManualResetEvent finishedEvent = new ManualResetEvent(false)) { for (int i = 0; i < threadCount; i++) { Action testAction = () => { TestPrimes(testsPerThread); if (Interlocked.Decrement(ref remaining) == 0) { finishedEvent.Set(); } }; if (useThreadPool) { ThreadPool.QueueUserWorkItem(s => testAction()); } else { ThreadStart ts = new ThreadStart(testAction); Thread th = new Thread(ts); th.Start(); } } finishedEvent.WaitOne(); } } [MethodImpl(MethodImplOptions.NoOptimization)] static void IteratePrimes(IEnumerable<int> primes) { int count = 0; foreach (int prime in primes) { count++; } } static void TestPrimes(int testRuns) { for (int t = 0; t < testRuns; t++) { var primes = Primes.GenerateUpTo(PrimeMax); IteratePrimes(primes); } } } }
А вот простой генератор:
using System; using System.Collections.Generic; using System.Linq; namespace ThreadingTest { public class Primes { public static IEnumerable<int> GenerateUpTo(int maxValue) { if (maxValue < 2) return Enumerable.Empty<int>(); bool[] primes = new bool[maxValue + 1]; for (int i = 2; i <= maxValue; i++) primes[i] = true; for (int i = 2; i < Math.Sqrt(maxValue + 1) + 1; i++) { if (primes[i]) { for (int j = i * i; j <= maxValue; j += i) primes[j] = false; } } return Enumerable.Range(2, maxValue - 1).Where(i => primes[i]); } } }
Если вы видите какие-либо очевидные недостатки в тесте, позвольте мне знать. За исключением каких-либо серьезных проблем с самим тестом, я думаю, что результаты говорят сами за себя, и сообщение ясно:
Не слушайте тех, кто делает слишком широкие и неквалифицированные заявления о том, что производительность .NET или любого другого языка/среды "плоха" в какой-то конкретной области, потому что они, вероятно, говорят из своей... задняя часть.
Возможно, вы захотите взглянуть на
System.Threading.Tasks
, представленную в .NET 4.Они ввели масштабируемый способ использования потоков с задачей с каким-то действительно крутым механизмом совместного использования заданий.
Кстати, я не знаю, кто сказал вам, что .NET не был хорош с параллелизмом. Все мои приложения используют потоки в какой-то момент другого, но не забывайте, что наличие 10 потоков на 2-ядерном процессоре является своего рода контрпродуктивным (в зависимости от типа задачи, которую вы заставляете их выполнять. Если это задачи, которые ждем сетевых ресурсов, тогда это может иметь смысл).
В любом случае, не бойтесь .NET для производительности, это на самом деле довольно хорошо.
Это миф. .NET очень хорошо справляется с управлением параллелизмом и очень масштабируема.
Если вы можете, я бы рекомендовал использовать .NET 4 и библиотеку параллельных задач. Это упрощает многие проблемы параллелизма. Для получения более подробной информации я бы рекомендовал обратиться к центру MSDN для параллельных вычислений с управляемым кодом.
Если вас интересуют детали реализации, у меня также есть очень подробный ряд по параллелизму в .NET .
Производительность .NET при параллелизме будет примерно такой же, как у приложений, написанных в машинном коде.
Тот, кто предупреждал вас, может заметить, что, поскольку многопоточные приложения гораздо проще писать в .NET, они иногда пишутся менее опытными программистами, которые не до конца понимают параллелизм, но это не является техническим ограничением.System.Threading
- это очень тонкий слой над потоковым API.Если анекдотические свидетельства помогут, на моей последней работе мы написали: сильно параллельное торговое приложение, которое обрабатывало более 20 000 событий рыночных данных в секунду и обновляло массивную сетку "основной формы" с соответствующими данными, все через довольно массивную потоковую архитектуру и все в C# и VB.NET из-за сложности приложения мы оптимизировали многие области, но никогда не видели преимущества в переписывании потокового кода на родном языке C++.
Во-первых, вы должны серьезно пересмотреть, нужно ли вам много потоков или только некоторые. Дело не в том, что .NET-потоки медленные. Потоки идут медленно. Переключение задач-дорогостоящая операция, независимо от того, кто написал алгоритм.
Это место, как и многие другие, где шаблоны проектирования могут помочь. Уже есть хорошие ответы, которые затрагивают этот факт, поэтому я просто сделаю его явным. Вам лучше использовать шаблон команд для маршалирования работы в несколько рабочих потоков, а затем выполнение этой работы как можно быстрее в последовательности, чем вы пытаетесь раскрутить кучу нитей и выполнить кучу работы "параллельно", которая на самом деле не выполняется параллельно, а, скорее, разделена на небольшие куски, которые сплетены вместе планировщиком.
Другими словами: вы лучше делите работу на куски ценности, используя свой ум и знания, чтобы решить, где границы между единицами ценности живут, чем позволяете какому-то общему решению, такому как операционная система решает за вас.