web-dev-qa-db-fra.com

Python obtenir des résultats significatifs à partir de cProfile

J'ai un script Python dans un fichier qui prend un peu plus de 30 secondes à exécuter. J'essaie de le profiler car je voudrais réduire cette fois de façon spectaculaire.

J'essaie de profiler le script en utilisant cProfile, mais essentiellement tout ce qu'il semble me dire, c'est que oui, le script principal a pris beaucoup de temps à s'exécuter, mais ne donne pas le genre de panne que j'attendais . Au terminal, je tape quelque chose comme:

cat my_script_input.txt | python -m cProfile -s time my_script.py

Les résultats que j'obtiens sont:

<my_script_output>
             683121 function calls (682169 primitive calls) in 32.133 seconds

   Ordered by: internal time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1   31.980   31.980   32.133   32.133 my_script.py:18(<module>)
   121089    0.050    0.000    0.050    0.000 {method 'split' of 'str' objects}
   121090    0.038    0.000    0.049    0.000 fileinput.py:243(next)
        2    0.027    0.014    0.036    0.018 {method 'sort' of 'list' objects}
   121089    0.009    0.000    0.009    0.000 {method 'strip' of 'str' objects}
   201534    0.009    0.000    0.009    0.000 {method 'append' of 'list' objects}
   100858    0.009    0.000    0.009    0.000 my_script.py:51(<lambda>)
      952    0.008    0.000    0.008    0.000 {method 'readlines' of 'file' objects}
 1904/952    0.003    0.000    0.011    0.000 fileinput.py:292(readline)
    14412    0.001    0.000    0.001    0.000 {method 'add' of 'set' objects}
      182    0.000    0.000    0.000    0.000 {method 'join' of 'str' objects}
        1    0.000    0.000    0.000    0.000 fileinput.py:80(<module>)
        1    0.000    0.000    0.000    0.000 fileinput.py:197(__init__)
        1    0.000    0.000    0.000    0.000 fileinput.py:266(nextfile)
        1    0.000    0.000    0.000    0.000 {isinstance}
        1    0.000    0.000    0.000    0.000 fileinput.py:91(input)
        1    0.000    0.000    0.000    0.000 fileinput.py:184(FileInput)
        1    0.000    0.000    0.000    0.000 fileinput.py:240(__iter__)
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

Cela ne semble rien me dire d'utile. La grande majorité du temps est simplement répertoriée comme:

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1   31.980   31.980   32.133   32.133 my_script.py:18(<module>)

Dans my_script.py, La ligne 18 n'est rien de plus que la fermeture """ du commentaire du bloc d'en-tête du fichier, donc ce n'est pas qu'il y ait toute une charge de travail concentrée dans la ligne 18. Le script dans son ensemble est principalement composé d'un traitement basé sur les lignes avec principalement du fractionnement, du tri et du réglage des chaînes, donc je m'attendais à trouver la majorité du temps pour une ou plusieurs de ces activités. Dans l'état actuel des choses, voir tout le temps groupé dans les résultats de cProfile comme se produisant sur une ligne de commentaire n'a aucun sens ou au moins ne fait pas la lumière sur ce qui est réellement consommant tout le temps.

EDIT: J'ai construit un exemple de travail minimum similaire à mon cas ci-dessus pour démontrer le même comportement:

mwe.py

import fileinput

for line in fileinput.input():
    for i in range(10):
        y = int(line.strip()) + int(line.strip())

Et appelez-le avec:

Perl -e 'for(1..1000000){print "$_\n"}' | python -m cProfile -s time mwe.py

Pour obtenir le résultat:

         22002536 function calls (22001694 primitive calls) in 9.433 seconds

   Ordered by: internal time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    8.004    8.004    9.433    9.433 mwe.py:1(<module>)
 20000000    1.021    0.000    1.021    0.000 {method 'strip' of 'str' objects}
  1000001    0.270    0.000    0.301    0.000 fileinput.py:243(next)
  1000000    0.107    0.000    0.107    0.000 {range}
      842    0.024    0.000    0.024    0.000 {method 'readlines' of 'file' objects}
 1684/842    0.007    0.000    0.032    0.000 fileinput.py:292(readline)
        1    0.000    0.000    0.000    0.000 fileinput.py:80(<module>)
        1    0.000    0.000    0.000    0.000 fileinput.py:91(input)
        1    0.000    0.000    0.000    0.000 fileinput.py:197(__init__)
        1    0.000    0.000    0.000    0.000 fileinput.py:184(FileInput)
        1    0.000    0.000    0.000    0.000 fileinput.py:266(nextfile)
        1    0.000    0.000    0.000    0.000 {isinstance}
        1    0.000    0.000    0.000    0.000 fileinput.py:240(__iter__)
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

Suis-je en train d'utiliser incorrectement cProfile?

32
Bryce Thomas

Comme je l'ai mentionné dans un commentaire, lorsque vous ne pouvez pas faire fonctionner cProfile en externe, vous pouvez souvent l'utiliser en interne à la place. Ce n'est pas si dur.

Par exemple, lorsque j'exécute avec -m cProfile Dans mon Python 2.7, j'obtiens effectivement les mêmes résultats que vous. Mais lorsque j'instrumente manuellement votre programme d'exemple:

import fileinput
import cProfile

pr = cProfile.Profile()
pr.enable()
for line in fileinput.input():
    for i in range(10):
        y = int(line.strip()) + int(line.strip())
pr.disable()
pr.print_stats(sort='time')

… Voici ce que j'obtiens:

         22002533 function calls (22001691 primitive calls) in 3.352 seconds

   Ordered by: internal time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
 20000000    2.326    0.000    2.326    0.000 {method 'strip' of 'str' objects}
  1000001    0.646    0.000    0.700    0.000 fileinput.py:243(next)
  1000000    0.325    0.000    0.325    0.000 {range}
      842    0.042    0.000    0.042    0.000 {method 'readlines' of 'file' objects}
 1684/842    0.013    0.000    0.055    0.000 fileinput.py:292(readline)
        1    0.000    0.000    0.000    0.000 fileinput.py:197(__init__)
        1    0.000    0.000    0.000    0.000 fileinput.py:91(input)
        1    0.000    0.000    0.000    0.000 {isinstance}
        1    0.000    0.000    0.000    0.000 fileinput.py:266(nextfile)
        1    0.000    0.000    0.000    0.000 fileinput.py:240(__iter__)
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

C'est beaucoup plus utile: il vous indique ce que vous attendiez probablement déjà, que plus de la moitié de votre temps est consacré à appeler str.strip().


Notez également que si vous ne pouvez pas modifier le fichier contenant le code que vous souhaitez profiler (mwe.py), Vous pouvez toujours le faire:

import cProfile
pr = cProfile.Profile()
pr.enable()
import mwe
pr.disable()
pr.print_stats(sort='time')

Même cela ne fonctionne pas toujours. Si votre programme appelle exit(), par exemple, vous devrez utiliser un wrapper try:/finally: Et/ou un atexit. Et il appelle os._exit(), ou segfaults, vous êtes probablement complètement arrosé. Mais ce n'est pas très courant.


Cependant, quelque chose que j'ai découvert plus tard: si vous déplacez tout le code hors de la portée globale, -m cProfile Semble fonctionner, au moins pour ce cas. Par exemple:

import fileinput

def f():
    for line in fileinput.input():
        for i in range(10):
            y = int(line.strip()) + int(line.strip())
f()

Maintenant, la sortie de -m cProfile Comprend, entre autres:

2000000 4.819 0.000 4.819 0.000: 0 (bande) 100001 0.288 0.000 0.295 0.000 fileinput.py:243(suivant)

Je ne sais pas pourquoi cela a également rendu deux fois plus lent… ou peut-être que c'est juste un effet de cache; cela fait quelques minutes depuis ma dernière exécution, et j'ai fait beaucoup de navigation sur le Web entre les deux. Mais ce n'est pas important, ce qui est important, c'est que la plupart du temps, les frais sont facturés à des endroits raisonnables.

Mais si je change cela pour déplacer la boucle externe au niveau global, et seulement son corps dans une fonction, la plupart du temps disparaît à nouveau.


Une autre alternative, que je ne suggérerais qu'en dernier recours…

Je remarque que si j'utilise profile au lieu de cProfile, cela fonctionne à la fois en interne et en externe, en facturant du temps aux bons appels. Cependant, ces appels sont également environ 5 fois plus lents. Et il semble y avoir 10 secondes supplémentaires de surcharge constante (qui sont facturées à import profile Si elles sont utilisées en interne, quelle que soit la ligne 1 si elles sont utilisées en externe). Donc, pour découvrir que split utilise 70% de mon temps, au lieu d'attendre 4 secondes et de faire 2.326/3.352, je dois attendre 27 secondes et faire 10.93/(26.34 - 10.01). Pas très amusant…


Une dernière chose: j'obtiens les mêmes résultats avec une version de développement CPython 3.4 - des résultats corrects lorsqu'ils sont utilisés en interne, tout est facturé à la première ligne de code lorsqu'il est utilisé en externe. Mais PyPy 2.2/2.7.3 et PyPy3 2.1b1/3.2.3 semblent tous deux me donner des résultats corrects avec -m cProfile. Cela peut simplement signifier que PyPy cProfile est truqué sur profile parce que le code pur-Python est assez rapide.


Quoi qu'il en soit, si quelqu'un peut comprendre/expliquer pourquoi -m cProfile Ne fonctionne pas, ce serait formidable ... mais sinon, c'est généralement une solution de contournement parfaite.

50
abarnert