2010-05-25 3 views
15

J'ai un programme avec de nombreux calculs indépendants, j'ai donc décidé de le paralléliser.Comprendre les résultats du profilage parallèle VS2010 C#

J'utilise Parallel.For/Each.

Les résultats étaient corrects pour une machine à deux noyaux - l'utilisation du processeur d'environ 80% à 90% la plupart du temps. Cependant, avec une machine Xeon double (c.-à-d. 8 cœurs), j'obtiens seulement environ 30% -40% d'utilisation du processeur, bien que le programme passe beaucoup de temps (parfois plus de 10 secondes) sur les sections parallèles. emploie environ 20 à 30 threads de plus dans ces sections par rapport aux sections en série. Chaque thread prend plus d'une seconde à compléter, 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 les verrous à un seul endroit, le profileur signale qu'environ 85% du temps du programme est consacré à la synchronisation (également 5-7% de sommeil, 5-7% d'exécution, moins de 1% d'E/S).

Le code verrouillé est seulement un cache (un dictionnaire) Inscrivez-vous/ajouter:

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 membre statique de la classe de type Object.
esn.populate lit à partir d'un fichier en utilisant un StreamReader distinct pour chaque thread.

Cependant, lorsque j'appuie sur le bouton Synchronisation pour voir ce qui cause le plus de retard, je vois que le profileur signale les lignes qui sont des lignes d'entrée de fonction, et ne signale pas les sections verrouillées elles-mêmes.
Il ne signale même pas la fonction qui contient le code ci-dessus (rappel - le seul verrouiller dans le programme) dans le cadre du profil de blocage avec le niveau de bruit 2%. Avec le niveau de bruit à 0%, il rapporte toutes les fonctions du programme, dont je ne comprends pas pourquoi elles comptent comme des synchronisations bloquantes.

Alors ma question est - que se passe-t-il ici?
Comment se fait-il que 85% du temps soit consacré à la synchronisation?
Comment puis-je savoir quel est le problème avec les sections parallèles de mon programme?

Merci. Après avoir exploré les threads (en utilisant le visualiseur extrêmement utile), j'ai découvert que la plus grande partie du temps de synchronisation était consacrée à l'attente de l'allocation de mémoire par le thread GC, et que des allocations fréquentes étaient nécessaires car des structures de données génériques redimensionnent les opérations.

Je vais devoir voir comment initialiser mes structures de données afin qu'elles allouent suffisamment de mémoire à l'initialisation, en évitant éventuellement cette course pour le thread GC.

Je rapporterai les résultats plus tard aujourd'hui.

Mise à jour: Il semble que les allocations de mémoire aient été la cause du problème. Lorsque j'ai utilisé des capacités initiales pour tous les dictionnaires et listes de la classe exécutée en parallèle, le problème de synchronisation était plus petit. Je n'avais maintenant que 80% de temps de synchronisation, avec des pics d'utilisation du processeur de 70% (les pics précédents n'étaient que d'environ 40%).J'ai foré encore plus loin dans chaque thread et découvert que maintenant beaucoup d'appels à l'allocation de GC ont été 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 d'objets préalloués, que j'utilise au lieu d'appeler la fonction "new".

J'ai donc implémenté un pool séparé de mémoire pour chaque thread, mais de manière très grossière, ce qui prend beaucoup de temps et n'est pas très bon - je dois encore utiliser beaucoup de nouveau pour l'initialisation de ces objets, seulement maintenant je le fais une fois globalement et il y a moins de conflit sur le thread GC, même quand il faut augmenter la taille du pool.

Mais ce n'est certainement pas une solution que j'aime car elle n'est pas généralisée facilement et je ne voudrais pas écrire mon propre gestionnaire de mémoire.
Existe-t-il un moyen d'indiquer à .NET d'allouer une quantité prédéfinie de mémoire pour chaque thread, puis de prendre toutes les allocations de mémoire du pool local?

+0

"alors je ne vois aucune raison pour qu'ils travaillent en parallèle" - avez-vous manqué un 'non' là-bas? –

+0

Par souci d'exhaustivité, quels sont les types lock_load_esn et cache? Et le cache est un membre statique, non? –

+0

Oups, j'ai oublié le "non", merci. lock_load_esn est Object (c'est-à-dire objet statique lock_load_esn = nouvel objet()) et cache est un wrapper Dictionary qui ne fait pas beaucoup plus que ce que fait un dictionnaire avec les méthodes TryGetValue/ContainsKey/Add, vraiment. – Haggai

Répondre

4

Pouvez-vous allouer moins?

J'ai eu quelques expériences similaires, en regardant la mauvaise performance et en découvrant le cœur du problème était le GC. Dans chaque cas, cependant, j'ai découvert que j'étais accidentellement hémorragique de la mémoire dans une boucle interne, allouant des tonnes d'objets temporaires inutilement. Je vais regarder attentivement le code et voir s'il y a des allocations que vous pouvez supprimer. Je pense qu'il est rare que les programmes aient besoin d'allouer beaucoup dans les boucles internes.

+0

Je crois que je pourrais allouer moins. Mais cela signifierait par exemple implémenter des arbres de structures complexes sous forme de tableaux multidimensionnels (au lieu d'allouer chaque noeud séparément), parallèlement à d'autres implémentations non intuitives de structures de données. Cela pourrait être fait, mais franchement - je ne crois pas que cela devrait être une solution générale de contention d'allocation de mémoire. – Haggai

+0

(Difficile à spéculer sans en savoir plus sur les détails de ces algorithmes/structures de données.) – Brian

+2

BTW, avez-vous exécuté le profileur de mémoire? Il pointe parfois vers des erreurs grossières, comme si vous voyiez un milliard de chaînes alloué, et réalisiez 'whoops', au lieu d'utiliser + sur les chaînes, je devrais utiliser un StringBuilder, ou autre chose. Je serais curieux de savoir exactement quelles structures de données sont allouées puis rapidement rejetées à l'intérieur de la boucle interne. – Brian