Перенос однопоточного приложения на многопоточное параллельное выполнение, моделирование Монте-Карло

Мне было поручено взять существующее однопоточное моделирование Монте-Карло и оптимизировать его. Это консольное приложение C #, без доступа к базе данных, оно загружает данные один раз из файла csv и записывает их в конце, поэтому оно в значительной степени ограничено процессором, также использует только около 50 МБ памяти.

Я запустил его через профилировщик Jetbrains dotTrace. Из общего времени выполнения около 30% приходится на генерацию однородных случайных чисел, 24% - на преобразование однородных случайных чисел в нормально распределенные случайные числа.

Базовый алгоритм представляет собой множество вложенных циклов for с вызовами случайных чисел и умножением матриц в центре, каждая итерация возвращает значение типа double, которое добавляется в список результатов, этот список периодически сортируется и проверяется. для некоторых критериев сходимости (в контрольных точках каждые 5% от общего числа итераций), если это приемлемо, программа выходит из цикла и записывает результаты, в противном случае она продолжается до конца.

Я бы хотел, чтобы разработчики отметили:

  • следует ли мне использовать новый поток v ThreadPool
  • стоит ли заглянуть в библиотеку расширений Microsoft Parallels
  • я должен посмотреть AForge.Net Parallel.For, http://code.google.com/p/aforge/ какие-либо другие библиотеки?

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

  • лучшие стратегии для генерации массовых нормально распределенных случайных чисел, а затем их использования. В этом состоянии приложение никогда не использует равномерные случайные числа, они всегда переводятся в нормально распределенные, а затем используются.
  • хорошие быстрые библиотеки (параллельные?) для генерации случайных чисел
  • Из соображений памяти, если я провожу эту параллель, сколько еще мне потребуется.

Текущее приложение занимает 2 часа на 500 000 итераций, бизнесу необходимо масштабировать его до 3 000 000 итераций и вызывать его несколько раз в день, поэтому необходима серьезная оптимизация.

Особо хотелось бы услышать мнение людей, которые использовали Microsoft Parallels Extension или AForge.Net Parallel

Это должно быть выполнено довольно быстро, поэтому бета-версия .net 4 отсутствует, хотя я знаю, что в нее встроены библиотеки параллелизма, мы можем рассмотреть возможность перехода на .net 4 позже, как только она будет выпущена. На данный момент на сервере есть .Net 2, я отправил на рассмотрение обновление до .net 3.5 SP1, которое есть в моем устройстве для разработчиков.

Спасибо

Обновить

Я только что пробовал реализацию Parallel.For, но она дает странные результаты. Однопоточный:

IRandomGenerator rnd = new MersenneTwister();
IDistribution dist = new DiscreteNormalDistribution(discreteNormalDistributionSize);
List<double> results = new List<double>();

for (int i = 0; i < CHECKPOINTS; i++)
{
 results.AddRange(Oblist.Simulate(rnd, dist, n));
}

To:

Parallel.For(0, CHECKPOINTS, i =>
        {
           results.AddRange(Oblist.Simulate(rnd, dist, n));
        });

Внутри имитации есть много вызовов rnd.nextUniform (), Я думаю, что получаю много одинаковых значений. Возможно ли это, потому что теперь это параллельное?

Также, возможно, проблемы с вызовом List AddRange не являются потокобезопасными? я вижу это

System.Threading.Collections.BlockingCollection, возможно, стоит использовать, но у него есть только метод Add без AddRange, поэтому мне пришлось бы просмотреть результаты и добавить потокобезопасным способом. Любое понимание от кого-то, кто использовал Parallel.For очень ценится. Я временно переключился на System.Random для своих вызовов, так как я получал исключение при вызове nextUniform с моей реализацией Mersenne Twister, возможно, это не было потокобезопасным определенным массивом получал индекс за пределами допустимого диапазона ....


person m3ntat    schedule 12.07.2009    source источник
comment
На какой машине вы его используете? Может получить часть необходимого увеличения скорости за счет обновленного оборудования.   -  person s_hewitt    schedule 12.07.2009
comment
Это на AMD opteron 275, 4 процессора, я думаю, не уверен, сколько ядер. Windows server 2003 SP2 32-разрядная   -  person m3ntat    schedule 12.07.2009


Ответы (3)


Сначала вам нужно понять, почему вы думаете, что использование нескольких потоков является оптимизацией, а на самом деле это не так. Использование нескольких потоков ускорит выполнение вашей рабочей нагрузки только, если у вас несколько процессоров, а затем не более чем в столько раз, сколько у вас есть в наличии (это называется ускорением ). Работа не «оптимизирована» в традиционном смысле этого слова (т.е. объем работы не уменьшается - фактически, при многопоточности общий объем работы обычно увеличивается из-за накладных расходов на многопоточность).

Таким образом, при разработке своего приложения вы должны найти части работы, которые можно выполнять параллельно или частично. Возможно, можно будет генерировать случайные числа параллельно (если несколько RNG будут работать на разных процессорах), но это также изменит результаты, поскольку вы получите разные случайные числа. Другой вариант - создать случайные числа на одном ЦП, а все остальное - на разных ЦП. Это может дать вам максимальное ускорение в 3 раза, так как ГСЧ по-прежнему будет работать последовательно и по-прежнему будет принимать 30% нагрузки.

Таким образом, если вы пойдете на это распараллеливание, вы получите 3 потока: поток 1 запускает RNG, поток 2 производит нормальное распределение, а поток 3 выполняет остальную часть моделирования.

Для этой архитектуры лучше всего подходит архитектура производитель-потребитель подходящее. Каждый поток будет читать свой ввод из очереди и передавать свой вывод в другую очередь. Каждая очередь должна быть блокирующей, поэтому, если поток ГСЧ отстает, поток нормализации будет автоматически блокироваться до тех пор, пока не станут доступны новые случайные числа. Для эффективности я бы передавал случайные числа в массиве, скажем, 100 (или больше) по потокам, чтобы избежать синхронизации для каждого случайного числа.

Для этого подхода вам не нужна продвинутая многопоточность. Просто используйте обычный класс потока, без пула или библиотеки. Единственное, что вам нужно (к сожалению) нет в стандартной библиотеке, - это класс блокировки Queue (класс Queue в System.Collections не годится). Codeproject предоставляет разумно выглядящую реализацию одного; наверное есть и другие.

person Martin v. Löwis    schedule 12.07.2009
comment
Другой вопрос, который следует учитывать, - это переключение контекста. Если бы вы не выбрали архитектуру, указанную выше (вероятно, это ошибка, из того, что вы сказали), тогда вы бы попытались выполнить много вычислений параллельно, что намного превысило бы количество ваших процессоров. Это было бы катастрофой, поскольку много процессорного времени, которое раньше вычисляло ответы, теперь тратится на переключение между потоками. Если бы у вас был какой-то файл io после каждого расчета, возможно, это можно было бы сделать асинхронно (но тогда вы бы использовали очередь и передавали элементы для хранения в специальный компонент). - person John Nicholas; 13.07.2009
comment
Расчет Монте-Карло полностью привязан к процессору, поэтому вы говорите, что я всегда должен сопоставлять 1 поток с 1 процессором на коробке, никогда не будет преимуществ в использовании ›1 потока на процессор? если один поток не ожидает чего-то еще, это позволит повысить эффективность переключений контекста, но в моем случае я думаю, что нет никакого преимущества, на самом деле это будет худшая производительность. - person m3ntat; 13.07.2009
comment
Правильный. Если в этих потоках действительно нет ввода-вывода, то использование нескольких потоков на ЦП замедлит его, а не ускорит. - person Martin v. Löwis; 13.07.2009
comment
Хорошо спасибо. В чем разница между ядром и процессором? также имеет ли значение гиперпоточность? Я разрабатывал и профилировал это на своей собственной машине: Intel core 2 duo E6550 @ 2,33 ГГц (в диспетчере устройств он отображается как 2 процессора). Сервер: AMD Opteron 275 (в диспетчере устройств отображается как 4 процессора). В С #, если я выполняю Environment.ProcessorCount и запускаю это количество потоков, это путь? Кроме того, если бы мне пришлось предложить новое оборудование для этого критически важного приложения на работе, о чем мне следует подумать. Спасибо - person m3ntat; 13.07.2009
comment
Также мой компьютер - Windows XP, сервер - Windows Server 2003 32 бит. Я только что прочитал об ОС Windows HPC и мне интересно, стоит ли оно того для этой миграции. Кроме того, если это приложение полностью связано с процессором, а не с памятью, вероятно, будет какое-то ускорение до x64, будут ли такие функции, как Math.Sqrt, умножение матриц, деление, быстрее на x64? - person m3ntat; 13.07.2009
comment
Вы должны рассматривать ЦП, ядро ​​и процессор как синонимы и подсчитывать только общее количество ядер (т.е. две микросхемы ЦП с каждыми двумя ядрами примерно равны одному ЦП с четырьмя ядрами). Windows отображает каждое ядро ​​(соответственно) как ЦП. С HyperThreading каждое ядро ​​может отображаться как два процессора; отключите HT, если это произойдет. Environment.ProcessorCount должен дать вам тот же номер, что и диспетчер задач. Игнорировать Windows HPC: это полезно только для кластеров из нескольких серверных компьютеров. x64: это действительно зависит от приложения. Для операций с плавающей запятой разницы быть не должно. - person Martin v. Löwis; 13.07.2009
comment
Еще одно замечание: не пытайтесь перестроить это приложение. Это действительно похоже на стандартный вариант использования многопоточности с несколькими разделяемыми заданиями, поэтому действительно старые, устоявшиеся подходы будут работать очень хорошо. Заставьте это работать, и трехкратное ускорение (на четырех процессорах) было бы превосходным. - person Martin v. Löwis; 13.07.2009

List<double> определенно не является потокобезопасным. См. Раздел «безопасность потоков» в документации System.Collections.Generic.List < / а>. Причина в производительности: добавление безопасности потоков платно.

Ваша реализация случайного числа также не является потокобезопасной; получение одних и тех же чисел несколько раз - это именно то, что вы ожидаете в этом случае. Давайте воспользуемся следующей упрощенной моделью rnd.NextUniform(), чтобы понять, что происходит:

  1. вычислить псевдослучайное число из текущего состояния объекта
  2. обновить состояние объекта, чтобы при следующем вызове получилось другое число
  3. вернуть псевдослучайное число

Теперь, если два потока выполняют этот метод параллельно, может произойти что-то вроде этого:

  • Поток A вычисляет случайное число, как на шаге 1.
  • Поток B вычисляет случайное число, как на шаге 1. Поток A еще не обновил состояние объекта, поэтому результат тот же.
  • Поток A обновляет состояние объекта, как на шаге 2.
  • Поток B обновляет состояние объекта, как на шаге 2, перекрывая изменения состояния A или, возможно, давая тот же результат.

Как видите, любые доводы, которые вы можете использовать, чтобы доказать, что rnd.NextUniform() работает, больше не действительны, потому что два потока мешают друг другу. Хуже того, подобные ошибки зависят от времени и могут проявляться очень редко как «сбои» при определенных рабочих нагрузках или в определенных системах. Кошмар отладки!

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

Другое (худшее) решение - создать поле, содержащее объект блокировки в вашем MersenneTwister классе, например:

private object lockObject = new object();

Затем используйте эту блокировку в своей MersenneTwister.NextUniform() реализации:

public double NextUniform()
{
   lock(lockObject)
   {
      // original code here
   }
}

Это предотвратит параллельное выполнение метода NextUniform () двумя потоками. Проблема со списком в вашем Parallel.For может быть решена аналогичным образом: разделите вызов Simulate и вызов AddRange, а затем добавьте блокировку вокруг вызова AddRange.

Моя рекомендация: по возможности избегайте совместного использования любого изменяемого состояния (например, состояния RNG) между параллельными задачами. Если изменяемое состояние не используется совместно, проблем с потоками не возникает. Это также позволяет избежать блокировки узких мест: вы не хотите, чтобы ваши «параллельные» задачи ожидали одного генератора случайных чисел, который вообще не работает параллельно. Особенно, если 30% времени тратится на поиск случайных чисел.

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

person Wim Coenen    schedule 13.07.2009
comment
фантастический ответ спасибо! В этом есть смысл. Теперь вопрос в том, следует ли мне использовать диапазон добавления или найти поточно-безопасную коллекцию, которая позволяет мне накапливать список случайных чисел (двойников), порядок добавления не важен, но мне нужно периодически сортировать результаты и получать результат с определенным процентилем и проверьте критерии сходимости, чтобы проверить раннее завершение симуляции, мне нужно сделать это для каждой параллели. Для запуска пути, а затем немедленно отменить все параллельные выполнения, если они выполнены, поскольку дальнейшая обработка не требуется, есть идеи, как это сделать? - person m3ntat; 13.07.2009
comment
Я не могу сразу ответить на этот вопрос. Периодические проверки статуса и отмена ожидающих / запущенных параллельных задач - отдельная большая тема; Я рекомендую вам задать новый вопрос. - person Wim Coenen; 13.07.2009

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

Библиотека параллельного расширения должна позволить вам распараллелить вашу программу, изменив некоторые из ваших циклов for на циклы Parallel.For. Если вы хотите увидеть, как это работает, Андерс Хейлсберг и Джо Даффи дают хорошее введение в своем 30-минутном видео здесь:

http://channel9.msdn.com/shows/Going+Deep/Programming-in-the-Age-of-Concurrency-Anders-Hejlsberg-and-Joe-Duffy-Concurrent-Programming-with/

Threading против ThreadPool

ThreadPool, как следует из названия, представляет собой пул потоков. Использование ThreadPool для получения ваших потоков имеет некоторые преимущества. Пул потоков позволяет более эффективно использовать потоки, предоставляя вашему приложению пул рабочих потоков, которыми управляет система.

person Robert Harvey    schedule 12.07.2009
comment
Хм, я не думаю, что использование ThreadPool будет более сложным, чем ручная работа с потоками - я думаю, это то, что вы хотели сказать, но упустили? При сравнении ThreadPool и ручной обработки потоков ThreadPool более эффективен (поскольку он перерабатывает завершенные потоки, создание потоков обходится дорого) и с ним немного легче работать, особенно при использовании делегатов. Тем не менее, я не могу говорить, чтобы сравнить его с параллельными библиотеками - просто не хотел, чтобы ThreadPool получил плохую репутацию :-) - person STW; 12.07.2009