Question 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 puis-je identifier les zones de mon code qui tournent lentement?


1498
2017-12-17 20:29


origine


Réponses:


Si votre objectif est d'utiliser un profileur, utilisez l'un des profils proposés.

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

Arrêtez-le plusieurs fois et regardez chaque fois la pile d'appels. S'il y a du code qui gaspille un certain pourcentage de temps, 20% ou 50% ou peu importe, c'est la probabilité que vous l'attrapiez dans la loi sur chaque échantillon. Donc, c'est à peu près le pourcentage d'échantillons sur lesquels vous le verrez. Il n'y a aucune supposition éduquée 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 nettoyez l'un d'entre eux, les autres prendront un plus grand pourcentage et seront plus faciles à repérer lors des passes suivantes. Ce effet de grossissement, lorsqu'il est composé sur plusieurs problèmes, peut conduire à des facteurs d'accélération vraiment massive.

Avertissement: Les programmeurs ont tendance à être sceptiques à l'égard de cette technique, sauf s'ils l'ont eux-mêmes utilisée. Ils diront que les profileurs vous donnent cette information, mais cela n'est vrai que s'ils échantillonnent la totalité de la pile d'appels, et vous permettent ensuite d'examiner un ensemble aléatoire d'échantillons. (Les résumés sont là où l'insight est perdu.) Les graphiques d'appel ne vous donnent pas la même information, car

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

Ils diront aussi que cela ne fonctionne que sur des programmes de jouets, alors qu'en réalité, cela fonctionne sur n'importe quel programme, et cela semble mieux fonctionner sur les programmes plus grands, parce qu'ils ont tendance à avoir plus de problèmes à trouver. Ils diront qu'il trouve parfois des choses qui ne sont pas des problèmes, mais cela n'est vrai que si vous voyez quelque chose une fois que. Si vous voyez un problème sur plus d'un échantillon, c'est réel.

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

P.P.S En général, plus vous avez de couches d'abstraction dans votre logiciel, plus vous avez de chances de trouver que c'est la cause des problèmes de performance (et la possibilité d'obtenir une accélération).

Ajouté: Cela peut ne pas être évident, mais la technique d'échantillonnage de la pile fonctionne aussi bien en présence de récursivité. La raison en est que le temps qui serait sauvé 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 dans un échantillon.

Une autre objection que j'entends souvent est: "Il s'arrêtera quelque part au hasard, et il manquera le vrai problème". Cela vient d'avoir un concept préalable de ce qu'est le vrai problème. Une propriété clé des problèmes de performance 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 un problème est réel, et vice-versa.

AJOUTÉ: Laissez-moi faire une explication bayésienne de la façon dont cela fonctionne. Supposons qu'il y a quelques instructions I (appel ou autrement) qui est sur l'appel empiler une fraction f de l'époque (et coûte donc beaucoup). Pour la simplicité, supposons que nous ne savons pas ce que f est, mais supposons qu'il est soit 0,1, 0,2, 0,3, ... 0,9, 1,0, et la probabilité a priori de chacune de ces possibilités est de 0,1, de sorte que tous ces coûts sont également probables a priori.

Supposons ensuite que nous prenons seulement 2 échantillons de piles, et nous voyons des instructions I sur les deux échantillons, observation désignée o=2/2. Cela nous donne de nouvelles estimations de la fréquence f de I, selon ce:

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)

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 dit que, par exemple, la probabilité que f > = 0,5 est de 92%, en hausse par rapport à l'hypothèse précédente de 60%.

Supposons que les hypothèses précédentes sont différentes. Supposons que nous supposions que P (f = 0.1) est .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)

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, il dit que P (f> = 0,5) est de 26%, en hausse par rapport à l'hypothèse précédente de 0,6%. Donc, Bayes nous permet 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'il est assez grand pour être réparé.

Pourtant, une autre façon de le regarder est appelé le Règle de Succession. Si vous lancez deux fois une pièce, et que cela revient deux fois, qu'est-ce que cela vous dit au sujet de la pondération probable de la pièce? La manière respectée de répondre est de dire que c'est une distribution bêta, avec une valeur moyenne (nombre de résultats + 1) / (nombre d'essais + 2) = (2 + 1) / (2 + 2) = 75%.

(La clé est que nous voyons I plus d'une fois. Si on ne le voit qu'une fois, ça ne nous dit pas grand-chose 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. n des échantillons sont prélevés, et f est le coût, alors I apparaîtra sur nf+/-sqrt(nf(1-f)) échantillons. Exemple, n=10, f=0.3, C'est 3+/-1.4 échantillons.)


AJOUTÉ, pour donner une idée intuitive de la différence entre la mesure et l'échantillonnage aléatoire de la pile:
Il y a maintenant des profileurs qui échantillonnent la pile, même à l'heure du mur, mais ce qui sort est une mesure (ou un 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) sont les échantillons eux-mêmes. Et si votre objectif est de trouver le goulot d'étranglement, le nombre d'entre eux que vous devez voir est, en moyenne, 2 divisé par la fraction de temps qu'il prend. Donc, si cela prend 30% du temps, 2 / .3 = 6,7 échantillons, en moyenne, le montreront, et la chance que 20 échantillons le montreront est de 99,2%.

Voici une illustration spontanée de la différence entre l'examen des mesures et l'examen des échantillons de la pile. Le goulot d'étranglement pourrait être un gros blob comme celui-ci, ou de nombreux petits, cela ne fait aucune différence.

enter image description here

La mesure est horizontale. il vous indique quelle fraction de routines spécifiques de temps prennent. L'échantillonnage est vertical. S'il y a un moyen d'éviter ce que tout le programme fait à ce moment-là, et si vous le voyez sur un deuxième échantillon, vous avez trouvé le goulot d'étranglement. C'est ce qui fait la différence - voir toute la raison du temps passé, pas seulement combien.


1192
2018-04-21 04:09



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 kcachegrind outil pour lire ce fichier. Il vous donnera une analyse graphique des choses avec des résultats comme les lignes coûtent combien.


472
2017-12-17 20:34



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 à propos de google-perftools. Cela vaut vraiment le coup d'essayer.

Question connexe ici.

Quelques autres mots à la mode si gprof ne fait pas le travail pour vous: Valgrind, Intel VTune, Soleil DTrace.


296
2017-08-17 11:48



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

Ceux-ci viennent avec des profileurs d'échantillonnage classiques (man-page) ainsi que le génial Tableau de temps!

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

Alt text


216
2018-05-22 21:44



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 essentiellement une machine virtuelle:

(wikipedia) Valgrind est par essence un virtuel   machine utilisant just-in-time (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   programme dans une forme temporaire, plus simple   appelé Représentation Intermédiaire   (IR), qui est neutre pour le processeur,   Formulaire SSA. Après la conversion,   un outil (voir ci-dessous) est libre de faire   quelles que soient les transformations qu'il aimerait   sur l'IR, avant que Valgrind ne traduise   l'IR de nouveau dans le code de la machine et permet   le processeur hôte l'exécute.

Callgrind est un profileur construit 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 un deuxième passage est suffisant pour obtenir des résultats solides et fiables, parce que Callgrind est un non-sondage profileur.

Un autre outil construit sur Valgrind est Massif. Je l'utilise pour profiler l'utilisation de la mémoire de tas. Cela fonctionne très bien. Qu'est-ce que cela fait, c'est qu'il vous donne des instantanés de l'utilisation de la mémoire - des informations détaillées QU'EST-CE QUE détient pourcentage de la mémoire, et que l'OMS avait mis là. Ces informations sont disponibles à différents moments de l'exécution de l'application.


65
2018-06-30 19:30



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

J'ai utilisé Gprof ces deux derniers jours et j'ai déjà trouvé trois limitations importantes, dont une que je n'ai jamais vue nulle part ailleurs (pour l'instant):

  1. Il ne fonctionne pas correctement sur le code multi-thread, sauf si vous utilisez un solution de contournement

  2. Le graphique d'appel est confus par les pointeurs de fonction. Exemple: J'ai une fonction appelée multithread () qui me permet de multi-thread une fonction spécifiée sur un tableau spécifié (les deux passés en arguments). Cependant, Gprof considère tous les appels à multithread () comme équivalents pour le calcul du temps passé dans les enfants. Puisque certaines fonctions que je passe à multithread () prennent beaucoup plus de temps que d'autres, mes graphiques d'appel sont souvent inutiles. (Pour ceux qui se demandent si le threading est le problème ici: non, multithread () peut éventuellement, et dans ce cas, exécuter tout de manière séquentielle sur le thread appelant uniquement).

  3. Ça dit ici que "... les chiffres du nombre d'appels sont calculés en comptant et non pas en faisant l'échantillonnage, ils sont complètement précis ...". Pourtant, je trouve mon graphique d'appel me donnant 5345859132 + 784984078 comme statistiques d'appel à ma fonction la plus appelée, où le premier nombre est censé être des appels directs, et les deuxièmes appels récursifs (qui sont tous de lui-même). Comme cela impliquait que j'avais un bug, j'ai mis des marqueurs longs (64 bits) dans le code et j'ai refait la même course. Mon compte: 5345859132 direct, et 78094395406 appels auto-récursifs. Il y a beaucoup de chiffres là-dessus, donc je rappellerai que les appels récursifs que je mesure sont de 78 milliards, contre 784m de Gprof: un facteur de 100 différent. Les deux exécutions étaient un code monothread 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 Debian Lenny 64 bits, si cela peut aider n'importe qui.


49
2018-06-08 08:01



La réponse à courir valgrind --tool=callgrind n'est pas tout à fait complet sans certaines options. Habituellement, nous ne voulons pas mettre en ligne 10 minutes de temps de démarrage lent sous Valgrind et nous voulons profiler notre programme quand il effectue une tâche.

Donc c'est ce que je recommande. Exécutez le programme en premier:

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

Maintenant, quand cela fonctionne et que nous voulons commencer le profilage, nous devrions courir 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

Maintenant, nous avons des fichiers nommés callgrind.out. * Dans le répertoire courant. Pour voir les résultats de profilage, utilisez:

kcachegrind callgrind.out.*

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


47
2018-02-23 21:28