Миграция однопоточного приложения в многопоточное, параллельное выполнение, симуляция Монте-Карло - PullRequest
8 голосов
/ 12 июля 2009

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

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

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

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

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

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

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

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

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

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

Спасибо

Обновление

Я только что попробовал реализацию 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));
}

Кому:

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, возможно, это не было безопасным для потоков , получал определенный массив индекс вне границ ....

Ответы [ 3 ]

13 голосов
/ 12 июля 2009

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

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

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

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

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

1 голос
/ 13 июля 2009

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

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

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

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

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

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

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

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

private object lockObject = new object();

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

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

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

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

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

0 голосов
/ 12 июля 2009

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

Библиотека параллельного расширения должна позволить вам распараллелить вашу программу, изменив некоторые из ваших циклов 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 для получения ваших потоков имеет ряд преимуществ. Пул потоков позволяет более эффективно использовать потоки, предоставляя вашему приложению пул рабочих потоков, управляемых системой.

...