J'ai un programme avec de nombreuses calculs indépendants donc j'ai décidé de le paralléliser.
J'utilise Parallel.For/Each.
Les résultats étaient corrects pour une machine dual-core - utilisation du CPU d'environ 80%-90% la plupart du temps. Cependant, avec une machine dual Xeon (c'est-à-dire 8 coeurs) j'obtiens seulement environ 30%-40% d'utilisation du CPU, même si le programme passe assez de temps (parfois plus de 10 secondes) sur les sections parallèles, et je vois qu'il emploie environ 20-30 threads de plus dans ces sections par rapport aux sections séquentielles. Chaque thread prend plus d'une seconde pour terminer, donc je ne vois aucune raison pour qu'ils ne travaillent pas en parallèle - à moins qu'il y ait un problème de synchronisation.
J'ai utilisé le profileur intégré de VS2010, et les résultats sont étranges. Même si j'utilise des verrous seulement à un endroit, le profileur rapporte que environ 85% du temps du programme est passé sur la synchronisation (également 5-7% en inactivité, 5-7% en exécution, moins de 1% en E/S).
Le code verrouillé est simplement un cache (un dictionnaire) get/add :
bool esn_found;
lock (lock_load_esn)
esn_found = cache.TryGetValue(st, out esn);
if(!esn_found)
{
esn = pData.esa_inv_idx.esa[term_idx];
esn.populate(pData.esa_inv_idx.datafile);
lock (lock_load_esn)
{
if (!cache.ContainsKey(st))
cache.Add(st, esn);
}
}
_lock_load_esn
est un membre statique de la classe de type Object.esn.populate
_ lit à partir d'un fichier en utilisant un StreamReader séparé pour chaque thread.
Cependant, quand j'appuie sur le bouton de Synchronisation pour voir ce qui cause le plus de retard, je vois que le profileur rapporte des lignes qui sont les lignes d'entrée des fonctions, et ne rapporte pas les sections verrouillées elles-mêmes.
Il ne rapporte même pas la fonction qui contient le code ci-dessus (rappel - le seul lock dans le programme) comme partie du profil de blocage avec un niveau de bruit de 2%. Avec un niveau de bruit à 0% il rapporte toutes les fonctions du programme, ce que je ne comprends pas pourquoi ils sont comptés comme synchronisations bloquantes.
Alors ma question est - que se passe-t-il ici?
Comment se fait-il que 85% du temps soit passé sur la synchronisation?
Comment puis-je découvrir quel est vraiment le problème avec les sections parallèles de mon programme?
Merci.
Mise à Jour : Après avoir examiné en détail les threads (en utilisant le visualiseur extrêmement utile) j'ai découvert que la plupart du temps de synchronisation était passé à attendre que le thread GC termine les allocations de mémoire, et que des allocations fréquentes étaient nécessaires en raison des opérations de redimensionnement de structures de données génériques.
Je dois voir comment initialiser mes structures de données pour qu'elles allouent suffisamment de mémoire dès l'initialisation, évitant ainsi cette course au thread GC.
Je rapporterai les résultats plus tard aujourd'hui.
Mise à Jour : Il semble que les allocations de mémoire étaient effectivement la cause du problème. Quand j'ai utilisé des capacités initiales pour tous les Dictionnaires et Listes dans la classe exécutée en parallèle, le problème de synchronisation a diminué. J'avais maintenant seulement environ 80% du temps de synchronisation, avec des pics d'utilisation du CPU de 70% (les pics précédents étaient seulement d'environ 40%).
J'ai creusé encore plus profondément dans chaque thread et découvert que maintenant de nombreux appels à GC allocate étaient faits pour allouer de petits objets qui ne faisaient pas partie des grands dictionnaires.
J'ai résolu ce problème en fournissant à chaque thread un pool de tels objets pré-alloués, que j'utilise au lieu d'appeler la fonction "new".
J'ai essentiellement implémenté un pool séparé de mémoire pour chaque thread, mais de manière très rudimentaire, ce qui est très chronophage et en réalité pas très bon - je dois encore utiliser beaucoup de new pour l'initialisation de ces objets, seulement maintenant je le fais une fois globalement et il y a moins de contention sur le thread GC, même en devant augmenter la taille du pool.
Mais ce n'est définitivement pas une solution que j'apprécie car ce n'est pas facilement généralisable et je n'aimerais pas écrire mon propre gestionnaire de mémoire.
Y a-t-il un moyen de dire à .NET d'allouer une quantité prédéfinie de mémoire pour chaque thread, et ensuite prendre toutes les allocations de mémoire depuis le pool local?