Migrer une application mono-thread vers une exécution parallèle multi-thread, simulation de monte carlo

J’ai été chargé de prendre une simulation existante de monte carlo à un seul thread et de l’ optimiser . Ceci est une application console #, aucun access à la firebase database, il charge les données une seule fois à partir d’un fichier csv et les écrit à la fin. C’est donc plutôt lié au processeur , il utilise également environ 50 Mo de mémoire.

Je l’ai fait passer par le profileur Jetbrains dotTrace. Environ 30% du temps d’exécution total génère des nombres aléatoires uniformes, 24% traduisant des nombres aléatoires uniformes en nombres aléatoires dissortingbués normalement.

L’ algorithme de base est constitué d’un grand nombre de boucles for nestedes , avec appels de nombres aléatoires et multiplication de masortingces au centre. Chaque itération renvoie un double qui est ajouté à une liste de résultats. Cette liste est périodiquement sortingée et testée pour certains critères de convergence points tous les 5% du nombre total d’itérations) si acceptable, le programme rompt les boucles et écrit les résultats, sinon il continue jusqu’à la fin.

J’aimerais que les développeurs prennent en compte:

  • dois-je utiliser le nouveau Thread v ThreadPool
  • dois-je regarder la bibliothèque d’extension Microsoft Parallels
  • Dois- je regarder AForge.Net Parallel.For , http://code.google.com/p/aforge/ n’importe quelle autre bibliothèque?

Quelques liens vers des tutoriels sur ce qui précède seraient les bienvenus car je n’ai jamais écrit de code parallèle ou multithread .

  • meilleures stratégies pour générer des nombres aléatoires normalement dissortingbués en masse, puis les consumr. Les nombres aléatoires uniformes ne sont jamais utilisés dans cet état par l’application, ils sont toujours traduits en fichiers normalement dissortingbués , puis consommés.
  • bonnes bibliothèques rapides (parallèles?) pour la génération de nombres aléatoires
  • Considérations de mémoire que je prends ce parallèle , combien supplémentaire aurai-je besoin.

L’application actuelle prend 2 heures pour 500 000 itérations, les entresockets ont besoin de cette capacité pour atteindre 3 000 000 d’itérations et être appelée plusieurs fois par jour. Une optimisation importante est donc nécessaire.

Particulary aimerait avoir des nouvelles de ceux qui ont utilisé Microsoft Parallels Extension ou AForge.Net Parallel

Cela doit être produit assez rapidement pour que .net 4 bêta soit sorti même si je sais qu’il possède des bibliothèques de simultanéité intégrées, nous pouvons envisager de migrer vers .net 4 plus tard une fois sorti. Pour le moment, le serveur a .Net 2, j’ai soumis à la révision une mise à jour de .net 3.5 SP1 dont dispose ma dev.

Merci

Mettre à jour

Je viens d’essayer l’implémentation de Parallel.For, mais cela donne des résultats étranges. Seul fileté:

IRandomGenerator rnd = new MersenneTwister(); IDissortingbution dist = new DiscreteNormalDissortingbution(discreteNormalDissortingbutionSize); List results = new List(); 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)); }); 

Dans simuler il y a beaucoup d’appels à rnd.nextUniform (), je pense que je reçois beaucoup de valeurs qui sont identiques , est-ce que cela va probablement arriver parce que c’est maintenant parallèle?

Peut-être aussi des problèmes avec l’appel List AddRange n’étant pas thread-safe? je vois ça

Il est peut-être utile d’utiliser System.Threading.Collections.BlockingCollection, mais il n’y a qu’une méthode Add, aucune AddRange, il faudrait donc que je vérifie les résultats et que nous ajoutions les threads en sécurité. Toute idée de quelqu’un qui a utilisé Parallel.For très apprécié. Je suis passé temporairement à System.Random pour mes appels, car je recevais une exception lors de l’appel de nextUniform avec mon implémentation Mersenne Twister. Peutêtre que ce n’était pas sécurisé, un certain tableau recevait un index hors limites

Vous devez d’abord comprendre pourquoi, selon vous, l’utilisation de plusieurs threads est une optimisation, alors que ce n’est pas le cas. L’utilisation de plusieurs threads accélère la charge de travail uniquement si vous avez plusieurs processeurs, et au maximum autant de fois que les processeurs disponibles ( accélération ). Le travail n’est pas “optimisé” au sens traditionnel du mot (c’est-à-dire que la quantité de travail n’est pas réduite – en fait, avec le multithreading, la quantité totale de travail augmente généralement à cause du temps de traitement).

Ainsi, lors de la conception de votre application, vous devez rechercher des tâches pouvant être effectuées de manière parallèle ou se chevauchant. Il peut être possible de générer des nombres aléatoires en parallèle (en ayant plusieurs RNG exécutés sur différentes CPU), mais cela changerait également les résultats, car vous obtenez des nombres aléatoires différents. Une autre option consiste à générer les nombres aléatoires sur un processeur et tout le rest sur différents processeurs. Cela peut vous donner une accélération maximale de 3, car le RNG fonctionnera toujours de manière séquentielle, tout en prenant 30% de la charge.

Donc, si vous optez pour cette parallélisation, vous vous retrouvez avec 3 threads: le thread 1 exécute le RNG, le thread 2 produit une dissortingbution normale et le thread 3 effectue le rest de la simulation.

Pour cette architecture, une architecture producteur-consommateur est la plus appropriée. Chaque thread lira son entrée d’une file d’attente et produira sa sortie dans une autre file d’attente. Chaque queue doit être bloquante. Ainsi, si le thread RNG prend du retard, le thread de normalisation se bloque automatiquement jusqu’à ce que de nouveaux nombres aléatoires soient disponibles. Pour plus d’efficacité, je passerais les nombres aléatoires dans un tableau de, disons, 100 (ou plus) entre les threads, afin d’éviter les synchronisations sur chaque nombre aléatoire.

Pour cette approche, vous n’avez besoin d’aucun thread avancé. Il suffit d’utiliser une classe de thread régulière, pas de pool, pas de bibliothèque. La seule chose dont vous avez besoin et qui (malheureusement) ne se trouve pas dans la bibliothèque standard est une classe de queue bloquante (la classe de queue dans System.Collections n’est pas bonne). Codeproject fournit une mise en œuvre assez réaliste de celle-ci; il y en a probablement d’autres.

List n’est définitivement pas thread-safe. Voir la section “Sécurité des threads” dans la documentation System.Collections.Generic.List . La raison en est la performance: l’ajout de la sécurité des threads n’est pas gratuit.

L’implémentation de votre nombre aléatoire n’est pas non plus thread-safe; Obtenir les mêmes numéros plusieurs fois est exactement ce que vous attendez dans ce cas. Utilisons le modèle simplifié suivant de rnd.NextUniform() pour comprendre ce qui se passe:

  1. calculer un nombre pseudo-aléatoire à partir de l’état actuel de l’object
  2. mise à jour de l’état de l’object pour que le prochain appel génère un numéro différent
  3. renvoyer le nombre pseudo-aléatoire

Maintenant, si deux threads exécutent cette méthode en parallèle, cela peut se produire:

  • Le fil A calcule un nombre aléatoire comme à l’étape 1.
  • Le thread B calcule un nombre aléatoire comme à l’étape 1. Le thread A n’a pas encore mis à jour l’état de l’object, le résultat est donc le même.
  • Le thread A met à jour l’état de l’object comme à l’étape 2.
  • Le fil d’exécution B met à jour l’état de l’object comme à l’étape 2, en piétinant les changements d’état de A ou en donnant peut-être le même résultat.

Comme vous pouvez le constater, tout raisonnement que vous pouvez faire pour prouver que rnd.NextUniform() fonctionne n’est plus valide car deux threads interfèrent l’un avec l’autre. Pire encore, de tels bogues dépendent du timing et peuvent apparaître rarement comme des “problèmes” sous certaines charges de travail ou sur certains systèmes. Débogage cauchemar!

Une solution possible consiste à éliminer le partage d’état: atsortingbuez à chaque tâche son propre générateur de nombres aléatoires initialisé avec une autre graine (en supposant que les instances ne partagent pas l’état par le biais de champs statiques).

Une autre solution (inférieure) consiste à créer un champ contenant un object verrou dans votre classe MersenneTwister , comme suit:

 private object lockObject = new object(); 

Utilisez ensuite ce verrou dans votre implémentation MersenneTwister.NextUniform() :

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

Cela empêchera deux threads d’exécuter la méthode NextUniform () en parallèle. Le problème de la liste dans votre Parallel.For peut être résolu de la même manière: séparez l’appel Simulate et l’appel AddRange , puis ajoutez un locking autour de l’appel AddRange .

Ma recommandation: évitez, dans la mesure du possible, de partager un état mutable (comme l’état RNG) entre des tâches parallèles. Si aucun état mutable n’est partagé, aucun problème de thread ne se produit. Cela évite également le blocage des goulots d’étranglement: vous ne voulez pas que vos tâches “parallèles” attendent sur un seul générateur de nombre aléatoire qui ne fonctionne pas du tout en parallèle. Surtout si 30% du temps est consacré à l’acquisition de nombres aléatoires.

Limitez le partage d’état et le locking aux endroits où vous ne pouvez pas l’éviter, comme lors de l’agrégation des résultats d’une exécution parallèle (comme dans vos appels AddRange ).

L’enfilage va être compliqué. Vous devrez diviser votre programme en unités logiques pouvant être exécutées sur leurs propres threads, et vous devrez gérer tous les problèmes de simultanéité qui surviennent.

Parallel Extension Library devrait vous permettre de paralléliser votre programme en remplaçant certaines de vos boucles for par des boucles Parallel.For . Si vous voulez voir comment cela fonctionne, Anders Hejlsberg et Joe Duffy fournissent une bonne introduction dans leur vidéo de 30 minutes ici:

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

Threading contre ThreadPool

Le ThreadPool, comme son nom l’indique, est un pool de threads. L’utilisation de ThreadPool pour obtenir vos threads présente certains avantages. Le regroupement de threads vous permet d’utiliser les threads plus efficacement en fournissant à votre application un pool de threads de travail gérés par le système.