.NET производительность параллелизма на стороне клиента - PullRequest
4 голосов
/ 22 марта 2010

Я пишу клиентское приложение .NET, которое, как ожидается, будет использовать много потоков. Меня предупредили, что производительность .NET очень плохая, когда речь идет о параллелизме. Хотя я не пишу приложение для реального времени, я хочу убедиться, что мое приложение является масштабируемым (то есть допускает много потоков) и каким-то образом сравнимо с эквивалентным приложением C ++.

Какой у вас опыт? Что такое соответствующий эталонный тест?

Ответы [ 5 ]

12 голосов
/ 27 марта 2010

Я собрал быстрый и грязный тест в C #, используя простой генератор в качестве теста.Тест генерирует простые числа до постоянного предела (я выбрал 500000), используя простую реализацию Sieve of Eratosthenes, и повторяет тест 800 раз, распараллеливаясь с определенным числом потоков, используя .NET ThreadPool или отдельные потоки.

Тест проводился на четырехъядерном Q6600 под управлением Windows Vista (x64).Это не использует Task Parallel Library, просто простые потоки.Он был запущен для следующих сценариев:

  • Последовательное выполнение (без потоков)
  • 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 или любой другой язык / среда "плохи" в какой-то конкретной области, потому что они, вероятно, говорят из своих ... задних частей.

9 голосов
/ 22 марта 2010

Возможно, вы захотите взглянуть на System.Threading.Tasks, представленный в .NET 4.

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

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

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

7 голосов
/ 22 марта 2010

Это миф..NET отлично справляется с управлением параллелизмом и является очень масштабируемым.

Если вы можете, я бы рекомендовал использовать .NET 4 и параллельную библиотеку задач.Это упрощает многие проблемы параллелизма.Для получения дополнительной информации я бы порекомендовал обратиться в центр MSDN для Параллельные вычисления с управляемым кодом .

Если вам интересны подробности реализации, у меня также есть очень подробная серия по Параллелизм в .NET .

4 голосов
/ 22 марта 2010

.NET производительность при параллельности будет примерно такой же, как у приложений, написанных на нативном коде. System.Threading - очень тонкий слой поверх API потоков.

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

Если случайные свидетельства помогут, на моей последней работе мы написали сильно параллельное торговое приложение, которое обрабатывало более 20 000 событий рыночных данных в секунду и обновляло массивную сетку «основной формы» с соответствующими данными, через довольно массивную многопоточную архитектуру и все в C # и VB.NET. Из-за сложности приложения мы оптимизировали многие области, но так и не увидели преимущества переписывания многопоточного кода на нативном C ++.

3 голосов
/ 22 марта 2010

Сначала вы должны серьезно пересмотреть вопрос о том, нужно ли вам много потоков или просто несколько.Дело не в том, что .NET потоки медленные.Темы медленные.Переключение задач - дорогостоящая операция, независимо от того, кто написал алгоритм.

Это место, как и многие другие, где могут помочь шаблоны проектирования.Уже есть хорошие ответы, которые касаются этого факта, так что я просто сделаю это явным.Вам лучше использовать шаблон команды, чтобы объединить работу в несколько рабочих потоков, а затем выполнить эту работу как можно быстрее по порядку, чем пытаться раскрутить кучу потоков и выполнить кучу работы «параллельно», котораяна самом деле это делается не параллельно, а, скорее, делится на маленькие кусочки, сплетенные планировщиком.

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

...