Параллельно. За большие издержки переключения контекста в этом случае ... Почему? - PullRequest
2 голосов
/ 05 апреля 2011

Я экспериментирую с новыми инструментами параллелизма в .NET 4, вычисляя Pi с использованием методов Монте-Карло.

(Фактический алгоритм не так важен, но для ясности, вот он:

  1. Выбор numIterations случайных точек внутри единичного квадрата.
  2. Подсчитайте количество этих точек, которые лежат внутри круга, ограниченного этим квадратом (то есть точек, расстояние от центра которых меньше 0,5)
  3. Тогда для очень больших numIterations, PI=4 * iterationsInsideCircle / numIterations.)

У меня есть метод int ThrowDarts(int numDarts), который выбирает numDarts случайные точки внутри единичного квадрата (описано выше) и возвращает количество точек, лежащих в единичном круге:

    protected static int ThrowDarts(int iterations)
    {
        int dartsInsideCircle = 0;
        Random random = new Random();
        for (int iteration = 0; iteration < iterations; iteration++)
        {
            double pointX = random.NextDouble() - 0.5;
            double pointY = random.NextDouble() - 0.5;

            double distanceFromOrigin = Math.Sqrt(pointX*pointX + pointY*pointY);
            bool pointInsideCircle = distanceFromOrigin <= 0.5;

            if (pointInsideCircle)
            {
                dartsInsideCircle++;
            }
        }
        return dartsInsideCircle;
    }

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

Например, моя однопоточная реализация просто:

    protected override int CountInterationsInsideCircle()
    {
        return ThrowDarts(_numInterations);
    }

У меня также есть этот метод для одного из моих параллельных алгоритмов:

    protected override int CountInterationsInsideCircle()
    {
        Task<int>[] tasks = new Task<int>[_numThreads];

        for (int i = 0; i < _numThreads; i++)
        {
            tasks[i] = Task.Factory.StartNew(() => ThrowDarts(_numInterations/_numThreads));
        }

        int iterationsInsideCircle = 0;
        for (int i = 0; i < _numThreads; i++)
        {
            iterationsInsideCircle += tasks[i].Result;
        }

        return iterationsInsideCircle;
    }

Надеюсь, вы получите картину.

Здесь я подхожу к своей головоломке. Версия Parallel.For, которую я пишу, вызывает огромное количество переключений контекста. Код ниже:

    protected override int CountInterationsInsideCircle()
    {
        ConcurrentBag<int> results = new ConcurrentBag<int>();
        int result = 0;

        Parallel.For(0, _numInterations,
                     // initialise each thread by setting it's hit count to 0
                     () => 0,
                     //in the body, we throw one dart and see whether it hit or not
                     (iteration, state, localState) => localState + ThrowDarts(1),
                     // finally, we sum (in a thread-safe way) all the hit counts of each thread together
                     results.Add);

        foreach(var threadresult in results)
        {
            result+=threadresult;
        }

        return result;
    }

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

Может ли кто-нибудь объяснить мне, почему это может происходить?

Ответы [ 4 ]

2 голосов
/ 05 апреля 2011

Я на самом деле нашел решение вопроса.

Ранее в моем методе ThrowDarts я создавал новый Random с каждым вызовом (это было потому, что класс Random не является потокобезопасным.)

Однако, оказывается, это относительно дорого. (По крайней мере, только при выполнении одного броска дротика мы генерируем новый Random для каждой итерации.)

Таким образом, я изменил свой ThrowDarts метод так, чтобы он принимал Random, который создает вызывающая сторона, и изменил мой LoopState, чтобы он содержал свой собственный объект Random.

Следовательно, каждый поток в Parallel.For содержит свой собственный Random. Моя новая реализация выглядит следующим образом:

    protected override int CountInterationsInsideCircle()
    {
        ConcurrentBag<int> results = new ConcurrentBag<int>();
        Parallel.For(0, _numInterations,
                     // initialise each thread by setting it's hit count to 0
                     () => new LoopThreadState(),
                     // in the body, we throw one dart and see whether it hit or not
                     (iteration, _, localState) =>
                        {
                            localState.Count += ThrowDarts(1, localState.RandomNumberGenerator);
                            return localState;
                        },
                     // finally, we sum (in a thread-safe way) all the hit counts of each thread together
                     result => results.Add(result.Count));

        int finalResult = 0;
        foreach (int threadresult in results)
        {
            finalResult += threadresult;
        }

        return finalResult;
    }

Я полагаю, что метрика переключения контекста была чем-то вроде красной селедки, и простой профиль помог бы. Хороший кривой мяч, .NET, хороший. Во всяком случае, урок усвоен!

Спасибо всем, Alex

0 голосов
/ 05 апреля 2011

Обновление Не могу не запустить те же тесты на моем домашнем ПК (linux 32bit, Q9550) с mono 2.8.2, а также просто для удовольствия :

[mono] /tmp @ dmcs MonteCarlo.cs 
[mono] /tmp @ time mono ./MonteCarlo.exe 
Yo
Approx: 392711899/500000000 => Pi: 3.141695192

real    0m28.109s
user    0m27.966s
sys 0m0.152s
[mono] /tmp @ dmcs MonteCarlo.cs # #define PARALLEL added
[mono] /tmp @ time mono ./MonteCarlo.exe 
Yo
Approx: 392687018/500000000 => Pi: 3.141496144

real    0m8.139s
user    0m31.506s
sys 0m0.064s

Так что да, похоже, масштабируется, как и ожидалось. Спасибо, что позволили мне на самом деле использовать это для моно. Это было в моем списке 'TODO' слишком долго, и это работает как шарм!

Оригинальный пост

Я только что рассчитал время, используя моно 2.8.2 на двухъядерной (E5300) Windows XP

Используя параллельную версию (#define PARALLEL), он работал за 40 с

При использовании последовательной версии (без определения PARALLEL) это заняло около 45 с.

Так что я не вижу твоих размеренных накладных расходов; или, по крайней мере, я не вижу замедления. Я также пропускаю ускорение, как и вы.

В параллельном прогоне я увидел, что оба ЦП привязаны на 100%, тогда как однопоточная версия использовала прибл. В среднем 50% обоих процессоров.

#define PARALLEL
using System;
using System.IO;
using System.Text.RegularExpressions;
using System.Collections.Concurrent;
using System.Threading.Tasks;
namespace test
{
    class MainClass
    {
        const int _numInterations = 50000;
        const int _dartsPerIter = 10000;

        protected static int ThrowDarts (int iterations)
        {
            Random random = new Random ();
            int dartsInsideCircle = 0;
            for (int iteration = 0; iteration < iterations; iteration++) {
                double pointX = random.NextDouble () - 0.5;
                double pointY = random.NextDouble () - 0.5;

                double distanceFromOrigin = Math.Sqrt (pointX * pointX + pointY * pointY);
                bool pointInsideCircle = distanceFromOrigin <= 0.5;

                if (pointInsideCircle) {
                    dartsInsideCircle++;
                }
            }
            return dartsInsideCircle;
        }
        protected int CountInterationsInsideCircle ()
        {
            ConcurrentBag<int> results = new ConcurrentBag<int> ();
            int result = 0;

            // initialise each thread by setting it's hit count to 0
            //in the body, we throw one dart and see whether it hit or not
            // finally, we sum (in a thread-safe way) all the hit counts of each thread together
#if PARALLEL
            Parallel.For (0, _numInterations, () => 0, (iteration, state, localState) => localState + ThrowDarts (_dartsPerIter), results.Add);
#else
            for (var i =0; i<_numInterations; ++i)
                results.Add(ThrowDarts (_dartsPerIter));
#endif

            foreach (var threadresult in results) {
                result += threadresult;
            }

            return result;
        }
        public static void Main (string[] args)
        {
            Console.WriteLine("Yo");
            var inside = new MainClass ().CountInterationsInsideCircle ();
            Console.WriteLine("Approx: {0}/{1} => Pi: {2}",
                               inside, _numInterations * _dartsPerIter,
                               (4.0*inside)/(1.0*_numInterations*_dartsPerIter));
        }
    }
}
0 голосов
/ 05 апреля 2011

Что происходит, когда _numThreads == _numIterations в вашем руководстве Задача?Первый подход специально разбивает его на _numThreads, где версия Parallel.For всегда будет создавать задачи _numIterations, по одной итерации каждая.В зависимости от количества итераций это может перегрузить пул потоков и свести на нет любые преимущества параллелизма из-за накладных расходов на конкуренцию за пул и связанную с ним блокировку.

Parallel.For хорошо подходит, когда каждая операция разумнодорогой и может быть рассчитан независимо.Проблема в этом случае заключается в том, что выполнение вычисления для одной итерации является дешевой операцией, поэтому накладные расходы начинают доминировать во времени для каждой задачи.Вы можете сделать вашу версию Parallel.For эквивалентной, используя _numThreads и _numIterations / _numThreads, так же, как вы делали это для ручной версии Задачи.

0 голосов
/ 05 апреля 2011

Гадание - в отличие от других реализаций, которые локально отслеживают свои результаты и затем объединяют их в конце, параллель для использует общий набор результатов, который заплатил бы более высокую цену, чтобы сохранить поток -защищенный, не говоря уже о том, что он, вероятно, страдает от общего доступа к строкам кэша (http://msdn.microsoft.com/en-us/magazine/cc872851.aspx).

...