web-dev-qa-db-fra.com

Comment puis-je profiler le code C++ sous Linux?

J'ai une application C++, fonctionnant sous Linux, que je suis en train d'optimiser. Comment identifier les zones de mon code qui fonctionnent lentement?

1601
Gabriel Isenberg

Si votre objectif est d'utiliser un profileur, utilisez l'une des méthodes suggérées.

Cependant, si vous êtes pressé et que vous pouvez interrompre manuellement votre programme sous le débogueur pendant qu'il est subjectivement lent, il existe un moyen simple de rechercher des problèmes de performances.

Arrêtez-le plusieurs fois et regardez à chaque fois la pile d'appels. S'il existe un code qui fait perdre un pourcentage du temps, 20% ou 50% ou autre, c'est la probabilité que vous le détectiez dans la loi de chaque échantillon. C’est donc à peu près le pourcentage d’échantillons sur lequel vous le verrez. Aucune conjecture éclairée n'est requise. Si vous avez une idée du problème, cela le prouvera ou le réfutera.

Vous pouvez avoir plusieurs problèmes de performances de différentes tailles. Si vous en nettoyez un, le pourcentage restant sera plus important et plus facile à repérer lors des passages suivants. Cet effet de grossissement , combiné à de multiples problèmes, peut conduire à des facteurs d'accélération vraiment énormes.

Mise en garde: les programmeurs ont tendance à être sceptiques à propos de cette technique à moins de l'avoir utilisée eux-mêmes. Ils vous diront que les profileurs vous fournissent ces informations, mais ce n'est vrai que s'ils échantillonnent la pile d'appels au complet, puis vous permettent d'examiner un ensemble d'échantillons aléatoire. (Les résumés sont les endroits où la perspicacité est perdue.) Les graphes d’appel ne vous donnent pas les mêmes informations, car

  1. ils ne résument pas au niveau de l'instruction, et
  2. ils donnent des résumés déroutants en présence de récursivité.

Ils diront également que cela ne fonctionne que sur les programmes de jouets, alors qu'en réalité, cela fonctionne mieux pour tous les programmes, et que cela semble mieux fonctionner pour les grands programmes, car ils ont généralement plus de problèmes à trouver. Ils diront qu'il trouve parfois des choses qui ne sont pas un problème, mais ce n'est vrai que si vous voyez quelque chose une fois . Si vous voyez un problème sur plusieurs échantillons, il est réel.

P.S. Cela peut également être fait sur les programmes multi-threads s'il existe un moyen de collecter des échantillons de pile d'appels du pool de threads à un moment donné, comme c'est le cas en Java.

P.P.S En règle générale, plus le nombre de couches d'abstraction dans votre logiciel est élevé, plus vous avez de chances de découvrir que c'est la cause de problèmes de performances (et l'occasion d'accélération).

Ajouté: Cela n’est peut-être pas évident, mais la technique d’échantillonnage de pile fonctionne tout aussi bien en présence de récursivité. La raison en est que le temps qui serait économisé par la suppression d’une instruction est approximé par la fraction d’échantillons qui la contient, quel que soit le nombre de fois où elle peut se produire au sein d’un échantillon.

Une autre objection que j’entends souvent est la suivante: " Cela s’arrêtera quelque part au hasard et le vrai problème sera oublié". Cela vient d'avoir un concept préalable de ce qu'est le vrai problème. Une caractéristique essentielle des problèmes de performances est qu'ils défient les attentes. L'échantillonnage vous dit que quelque chose est un problème et votre première réaction est l'incrédulité. C'est naturel, mais vous pouvez être sûr que si le problème est réel, et inversement.

AJOUTÉ: Permettez-moi de faire une explication bayésienne de la façon dont cela fonctionne. Supposons qu'il existe une instruction I (appel ou autre) qui figure sur la pile d'appels une fraction du temps f du temps (et qui coûte donc très cher). Par souci de simplicité, supposons que nous ne sachions pas ce que f est, mais supposons que ce soit 0,1, 0,2, 0,3, ... 0,9, 1,0 et que la probabilité a priori de chacune de ces possibilités soit de 0,1; ces coûts sont également probables a priori.

Supposons alors que nous ne prenons que 2 échantillons de pile et que nous voyons l'instruction I sur les deux échantillons, appelée observation o=2/2. Cela nous donne de nouvelles estimations de la fréquence f de I, selon ceci:

Prior                                    
P(f=x) x  P(o=2/2|f=x) P(o=2/2&&f=x)  P(o=2/2&&f >= x)  P(f >= x | o=2/2)

0.1    1     1             0.1          0.1            0.25974026
0.1    0.9   0.81          0.081        0.181          0.47012987
0.1    0.8   0.64          0.064        0.245          0.636363636
0.1    0.7   0.49          0.049        0.294          0.763636364
0.1    0.6   0.36          0.036        0.33           0.857142857
0.1    0.5   0.25          0.025        0.355          0.922077922
0.1    0.4   0.16          0.016        0.371          0.963636364
0.1    0.3   0.09          0.009        0.38           0.987012987
0.1    0.2   0.04          0.004        0.384          0.997402597
0.1    0.1   0.01          0.001        0.385          1

                  P(o=2/2) 0.385                

La dernière colonne indique que, par exemple, la probabilité que f> = 0,5 soit de 92%, en hausse par rapport à l'hypothèse antérieure de 60%.

Supposons que les hypothèses antérieures sont différentes. Supposons que nous supposons que P (f = 0,1) vaut 0,991 (presque certain) et que toutes les autres possibilités sont presque impossibles (0,001). En d’autres termes, notre certitude préalable est que I est bon marché. Ensuite nous obtenons:

Prior                                    
P(f=x) x  P(o=2/2|f=x) P(o=2/2&& f=x)  P(o=2/2&&f >= x)  P(f >= x | o=2/2)

0.001  1    1              0.001        0.001          0.072727273
0.001  0.9  0.81           0.00081      0.00181        0.131636364
0.001  0.8  0.64           0.00064      0.00245        0.178181818
0.001  0.7  0.49           0.00049      0.00294        0.213818182
0.001  0.6  0.36           0.00036      0.0033         0.24
0.001  0.5  0.25           0.00025      0.00355        0.258181818
0.001  0.4  0.16           0.00016      0.00371        0.269818182
0.001  0.3  0.09           0.00009      0.0038         0.276363636
0.001  0.2  0.04           0.00004      0.00384        0.279272727
0.991  0.1  0.01           0.00991      0.01375        1

                  P(o=2/2) 0.01375                

Maintenant, on dit que P (f> = 0,5) est de 26%, en hausse par rapport à l'hypothèse antérieure de 0,6%. Bayes nous permet donc de mettre à jour notre estimation du coût probable de I. Si la quantité de données est petite, cela ne nous dit pas exactement quel est le coût, mais seulement qu’elles sont assez grandes pour qu’elles valent la peine d’être corrigées.

Une autre façon de voir les choses est appelée règle de succession . Si vous lancez une pièce de monnaie deux fois, et que cela revient deux fois, qu'est-ce que cela vous dit sur la pondération probable de la pièce? La manière respectée de répondre est de dire que c'est une distribution Beta, avec une valeur moyenne (nombre de hits + 1)/(nombre de tentatives + 2) = (2 + 1)/(2 + 2) = 75%.

(La clé est que nous voyons I plus d'une fois. Si nous ne le voyons qu'une fois, cela ne nous en dit pas beaucoup, sauf que f> 0.)

Ainsi, même un très petit nombre d'échantillons peut nous en dire beaucoup sur le coût des instructions qu'il voit. (Et il les verra avec une fréquence, en moyenne, proportionnelle à leur coût. Si n échantillons sont pris et que f est le coût, alors I apparaîtra sur nf+/-sqrt(nf(1-f)) échantillons. Exemple, n=10, f=0.3, c'est-à-dire 3+/-1.4 échantillons.)


ADDED, pour donner une idée intuitive de la différence entre un échantillonnage aléatoire et un échantillonnage aléatoire:
Il existe maintenant des profileurs qui échantillonnent la pile, même à une heure donnée, mais ce qui sort correspond à des mesures (ou chemin chaud), ou un point chaud, à partir duquel un "goulot d'étranglement" peut facilement se cacher). Ce qu'ils ne vous montrent pas (et ils pourraient facilement), ce sont les échantillons eux-mêmes. Et si votre objectif est de trouver le goulot d’étranglement, le nombre d’entre eux dont vous avez besoin est de en moyenne , 2 divisé par la fraction de temps qu'il faut. Donc, si cela prend 30% du temps, 2/.3 = 6,7 échantillons en moyenne le montreront, et la probabilité que 20 échantillons le montrent est de 99,2%.

Voici une illustration spontanée de la différence entre l'examen des mesures et l'examen des échantillons de pile. Le goulot d'étranglement pourrait être une grosse tâche comme celle-ci, ou de nombreuses petites, cela ne fait aucune différence.

enter image description here

La mesure est horizontale; il vous indique quelle fraction de temps prend une routine. L'échantillonnage est vertical. S'il existe un moyen d'éviter ce que fait tout le programme à ce moment-là, et si vous le voyez sur un deuxième échantillon , vous avez trouvé le goulot. C'est ce qui fait la différence - voir la raison complète du temps passé, pas seulement la quantité.

1331
Mike Dunlavey

Vous pouvez utiliser Valgrind avec les options suivantes

valgrind --tool=callgrind ./(Your binary)

Il va générer un fichier appelé callgrind.out.x. Vous pouvez ensuite utiliser l'outil kcachegrind pour lire ce fichier. Cela vous donnera une analyse graphique de choses avec des résultats comme quelles lignes coûtent combien. 

505
Ajay

Je suppose que vous utilisez GCC. La solution standard serait de profiler avec gprof .

Assurez-vous d’ajouter -pg à la compilation avant le profilage:

cc -o myprog myprog.c utils.c -g -pg

Je ne l'ai pas encore essayé, mais j'ai entendu de bonnes choses sur google-perftools . Cela vaut vraiment la peine d'essayer.

Question connexe ici .

Quelques autres mots à la mode si gprof ne fait pas le travail à votre place: Valgrind , Intel VTune , Sun DTrace .

316
Nazgob

Les nouveaux noyaux (par exemple, les derniers noyaux Ubuntu) sont livrés avec les nouveaux outils 'perf' (apt-get install linux-tools) AKA perf_events .

Ceux-ci viennent avec les profileurs d'échantillonnage classiques ( page de manuel ) ainsi que le génial timechart !

L'important est que ces outils puissent être le profilage système et pas seulement le profilage de processus - ils peuvent montrer l'interaction entre les threads, les processus et le noyau et vous permettre de comprendre la planification et les dépendances d'E/S entre processus.

Alt text

234
Will

J'utiliserais Valgrind et Callgrind comme base pour ma suite d'outils de profilage. Ce qu'il est important de savoir, c'est que Valgrind est fondamentalement une machine virtuelle:

(Wikipédia) Valgrind est essentiellement un virtuel machine utilisant juste à temps (JIT) techniques de compilation, y compris recompilation dynamique. Rien de le programme original est exécuté directement sur le processeur hôte . Au lieu de cela, Valgrind traduit d’abord le fichier programme dans une forme temporaire, plus simple appelé représentation intermédiaire (IR), qui est un processeur neutre, Formulaire basé sur SSA. Après la conversion, un outil (voir ci-dessous) est gratuit quelles que soient les transformations souhaitées sur l'IR, avant que Valgrind ne traduise l’IR dans le code machine et laisse le processeur hôte l'exécute. 

Callgrind est un profileur basé sur cela. Le principal avantage est que vous n'avez pas à exécuter votre application pendant des heures pour obtenir un résultat fiable. Même une seconde est suffisante pour obtenir des résultats solides et fiables, car Callgrind est un profileur non-probing

Massif est un autre outil construit sur Valgrind. Je l'utilise pour profiler l'utilisation de la mémoire de tas. Cela fonctionne très bien. Ce qu'il fait, c'est qu'il vous donne des instantanés de l'utilisation de la mémoire - informations détaillées QU'EST-CE QU'UN pourcentage de mémoire, QUI l'a mis. Ces informations sont disponibles à différents moments de l'exécution de l'application.

68
anon

La solution pour exécuter valgrind --tool=callgrind n'est pas tout à fait complète sans certaines options. Nous ne souhaitons généralement pas profiler 10 minutes de temps de démarrage lent sous Valgrind et souhaitons profiler notre programme lorsqu'il effectue une tâche.

C'est donc ce que je recommande. Lancer le programme en premier:

valgrind --tool=callgrind --dump-instr=yes -v --instr-atstart=no ./binary > tmp

Maintenant, lorsque cela fonctionne et que nous voulons commencer le profilage, nous devrions l'exécuter dans une autre fenêtre:

callgrind_control -i on

Cela active le profilage. Pour l'éteindre et arrêter toute la tâche, nous pourrions utiliser:

callgrind_control -k

Nous avons maintenant des fichiers nommés callgrind.out. * Dans le répertoire en cours. Pour voir les résultats du profilage, utilisez:

kcachegrind callgrind.out.*

Je recommande dans la fenêtre suivante de cliquer sur l'en-tête de colonne "Self", sinon cela indique que "main ()" est la tâche qui prend le plus de temps. "Self" indique combien chaque fonction a pris du temps, et non avec les personnes à charge. 

55
Tõnu Samuel

Ceci est une réponse à la réponse Gprof de Nazgob .

J'utilise Gprof depuis quelques jours et j'ai déjà constaté trois limitations importantes, dont l'une n'a jamais été documentée nulle part ailleurs:

  1. Cela ne fonctionne pas correctement sur du code multithread, sauf si vous utilisez une solution de contournement

  2. Le graphe d'appel est confondu par les pointeurs de fonction. Exemple: J'ai une fonction appelée multithread() qui me permet de traiter plusieurs fois une fonction spécifiée sur un tableau spécifié (les deux étant transmises en tant qu'arguments). Cependant, Gprof considère que tous les appels à multithread() sont équivalents aux fins du calcul du temps passé chez les enfants. Étant donné que certaines fonctions que je transmets à multithread() prennent beaucoup plus de temps que d’autres, mes graphes d’appel sont généralement inutiles. (Pour ceux qui se demandent si le filetage est le problème ici: non, multithread() peut éventuellement, et dans ce cas, tout exécuter de manière séquentielle sur le fil d'appel uniquement).

  3. Il est dit ici que "... les chiffres relatifs au nombre d’appels sont calculés par comptage et non par échantillonnage. Ils sont tout à fait exacts ...". Pourtant, je trouve dans mon graphe d'appels que 5345859132 + 784984078 sont des statistiques d'appel de la fonction la plus appelée, où le premier numéro est supposé être des appels directs et le second des appels récursifs (qui proviennent tous de lui-même). Comme cela impliquait un bogue, j'ai inséré des compteurs longs (64 bits) dans le code et refait la même chose. Mes comptes: 5345859132 appels directs et 78094395406 auto-récursifs. Il y a beaucoup de chiffres, alors je soulignerai que les appels récursifs que je mesure sont 78 milliards, contre 784 millions de Gprof: un facteur de 100 différent. Les deux exécutions étaient à code unique et non optimisé, l’un compilé -g et l’autre -pg.

C'était GNU Gprof (GNU Binutils pour Debian) 2.18.0.20080103 fonctionnant sous Lenny 64 bits, si cela peut aider quelqu'un.

53
Rob_before_edits

Utilisez Valgrind, callgrind et kcachegrind:  

valgrind --tool=callgrind ./(Your binary)

génère callgrind.out.x. Lisez-le en utilisant kcachegrind.

Utilisez gprof (add -pg):  

cc -o myprog myprog.c utils.c -g -pg 

(pas très bon pour les multi-threads, les pointeurs de fonction)

Utilisez google-perftools:  

Utilise l'échantillonnage du temps, les goulots d'étranglement d'E/S et de l'UC sont révélés.

Intel VTune est le meilleur (gratuit à des fins éducatives).

Autres: AMD Codeanalyst (remplacé depuis par AMD CodeXL), OProfile, outils 'perf' (apt-get install linux-tools)

14
Ehsan

Pour les programmes à un seul thread, vous pouvez utiliser igprof, The Ignominous Profiler: https://igprof.org/ .

Il s’agit d’un profileur d’échantillonnage, inspiré de la réponse ... longue ... par Mike Dunlavey, qui présentera les résultats dans un arbre de pile d’appels parcourable, annoté avec le temps ou la mémoire passée dans chaque fonction, de par fonction.

4
fwyzard

Voici les deux méthodes que j'utilise pour accélérer mon code:

Pour les applications liées à la CPU:

  1. Utilisez un profileur en mode DEBUG pour identifier les parties discutables de votre code
  2. Ensuite, passez en mode RELEASE et commentez les sections discutables de votre code (stubez-le sans rien) jusqu'à ce que vous constatiez des changements dans les performances.

Pour les applications liées aux E/S:

  1. Utilisez un profileur en mode RELEASE pour identifier les parties discutables de votre code.

N.B.

Si vous n'avez pas de profileur, utilisez le profileur du pauvre. Appuyez sur pause lors du débogage de votre application. La plupart des suites de développeurs perceront Assembly avec des numéros de ligne commentés. Vous êtes statistiquement susceptible d'atterrir dans une région qui consomme la plupart de vos cycles de processeur.

Pour le processeur, le profilage en modeDEBUGest dû au fait que si vous tentez de profiler en modeRELEASE, le compilateur va réduire les calculs, les boucles de vectorisation et les fonctions en ligne votre code dans un désordre non mappable quand il est assemblé. Un désordre non mappable signifie que votre profileur ne sera pas en mesure d'identifier clairement ce qui prend si longtemps, car Assembly risque de ne pas correspondre au code source sous optimisation . Si vous avez besoin des performances (par exemple, sensibles au minutage) du modeRELEASE, désactivez les fonctions du débogueur si nécessaire pour conserver des performances utilisables.

Pour les interfaces d'E/S, le profileur peut toujours identifier les opérations d'E/S en modeRELEASEcar les opérations d'E/S sont soit liées de manière externe à une bibliothèque partagée (la plupart du temps), soit dans le pire des cas, se traduira par un vecteur d’interruption sys-call (facilement identifiable par le profileur).

2
seo

Il convient également de mentionner sont

  1. HPCToolkit ( http://hpctoolkit.org/ ) - Code source ouvert, fonctionne pour les programmes parallèles et dispose d'une interface graphique permettant d'examiner les résultats de différentes manières.
  2. Intel VTune ( https://software.intel.com/en-us/vtune ) - Si vous avez des compilateurs Intel, c'est très bien 
  3. TAU ( http://www.cs.uoregon.edu/research/tau/home.php

J'ai utilisé HPCToolkit et VTune et ils sont très efficaces pour trouver le long pôle dans la tente et n'ont pas besoin de recompiler votre code (sauf que vous devez utiliser le type -g -O ou RelWithDebInfo intégré dans CMake pour obtenir un résultat significatif) . J'ai entendu dire que TAU avait des capacités similaires.

1
raovgarimella

Vous pouvez utiliser un cadre de journalisation tel que loguru , car il inclut les horodatages et la disponibilité totale, qui peuvent être utilisés de manière conviviale pour le profilage:

  

0
BullyWiiPlaza

Vous pouvez utiliser la bibliothèque iprof:

https://gitlab.com/Neurochrom/iprof

https://github.com/Neurochrom/iprof

Il est multi-plateforme et vous permet de ne pas mesurer les performances de votre application également en temps réel. Vous pouvez même le coupler avec un graphe en direct . Disclaimer: Je suis l'auteur.

0
N3UR0CHR0M

Au travail, nous avons un outil vraiment agréable qui nous aide à surveiller ce que nous voulons en termes de planification. Cela a été utile de nombreuses fois.

Il est en C++ et doit être personnalisé selon vos besoins. Malheureusement, je ne peux pas partager de code, juste des concepts. Vous utilisez un tampon "volumineux" volatile contenant des horodatages et un ID d'événement que vous pouvez vider post mortem ou après avoir arrêté le système de journalisation (et le déposer dans un fichier par exemple).

Vous récupérez le tampon volumineux avec toutes les données et une petite interface l’analyse et affiche les événements avec le nom (haut/bas + valeur) comme le fait un oscilloscope avec les couleurs (configuré dans le fichier .hpp).

Vous personnalisez le nombre d'événements générés pour vous concentrer uniquement sur ce que vous désirez. Cela nous a beaucoup aidé pour les problèmes de planification tout en consommant la quantité de CPU souhaitée en fonction du nombre d'événements enregistrés par seconde.

Vous avez besoin de 3 fichiers:

toolname.hpp // interface
toolname.cpp // code
tool_events_id.hpp // Events ID

Le concept est de définir les événements dans tool_events_id.hpp comme ceci:

// EVENT_NAME                         ID      BEGIN_END BG_COLOR NAME
#define SOCK_PDU_RECV_D               0x0301  //@D00301 BGEEAAAA # TX_PDU_Recv
#define SOCK_PDU_RECV_F               0x0302  //@F00301 BGEEAAAA # TX_PDU_Recv

Vous définissez également quelques fonctions dans toolname.hpp:

#define LOG_LEVEL_ERROR 0
#define LOG_LEVEL_WARN 1
// ...

void init(void);
void probe(id,payload);
// etc

Où que vous soyez dans votre code, vous pouvez utiliser:

toolname<LOG_LEVEL>::log(EVENT_NAME,VALUE);

La fonction probe utilise quelques lignes d'assemblage pour extraire l'horodatage de l'horloge dès que possible, puis définit une entrée dans la mémoire tampon. Nous avons également un incrément atomique pour trouver en toute sécurité un index où stocker l'événement de journal. Bien sûr, le tampon est circulaire.

J'espère que l'idée n'est pas obscurcie par le manque d'échantillons de code.

0
SOKS

Comme personne n’a mentionné Arm MAP, j’ajouterais que personnellement, j’ai utilisé avec succès Map pour profiler un programme scientifique C++.

Arm MAP est le profileur pour les codes C, C++, Fortran et F90 à thread unique, multithread ou mono-thread. Il fournit une analyse approfondie et une identification précise des goulots d'étranglement par rapport à la ligne source. Contrairement à la plupart des profileurs, il est conçu pour pouvoir profiler des pthreads, OpenMP ou MPI pour du code parallèle et threadé.

MAP est un logiciel commercial.

0
Wei