web-dev-qa-db-fra.com

Comment savoir quelles parties de mon code sont inefficaces dans Python

Sur une question précédente, je posais des questions sur le multitraitement, l'utilisation de plusieurs cœurs pour accélérer l'exécution d'un programme, et quelqu'un m'a dit ceci:

Plus souvent qu'autrement, vous pouvez obtenir une optimisation 100x + avec un meilleur code par rapport à une amélioration 4x et des complexités supplémentaires avec le multitraitement

Ils m'ont ensuite recommandé:

Utilisez un profileur pour comprendre ce qui est lent, puis concentrez-vous sur l'optimisation de cela.

Je suis donc allé à cette question: Comment pouvez-vous profiler un script?

Ici, j'ai trouvé cProfile et l'ai implémenté dans du code de test pour voir comment cela fonctionne.

Voici mon code:

import cProfile

def foo():
    for i in range(10000):
        a = i**i
        if i % 1000 == 0:
            print(i)

cProfile.run('foo()')

Cependant, après l'avoir exécuté, c'est ce que j'ai obtenu:

0
1000
2000
3000
4000
5000
6000
7000
8000
9000
         1018 function calls in 20.773 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000   20.773   20.773 <string>:1(<module>)
      147    0.000    0.000    0.000    0.000 rpc.py:150(debug)
       21    0.000    0.000    0.050    0.002 rpc.py:213(remotecall)
       21    0.000    0.000    0.002    0.000 rpc.py:223(asynccall)
       21    0.000    0.000    0.048    0.002 rpc.py:243(asyncreturn)
       21    0.000    0.000    0.000    0.000 rpc.py:249(decoderesponse)
       21    0.000    0.000    0.048    0.002 rpc.py:287(getresponse)
       21    0.000    0.000    0.000    0.000 rpc.py:295(_proxify)
       21    0.001    0.000    0.048    0.002 rpc.py:303(_getresponse)
       21    0.000    0.000    0.000    0.000 rpc.py:325(newseq)
       21    0.000    0.000    0.002    0.000 rpc.py:329(putmessage)
       21    0.000    0.000    0.000    0.000 rpc.py:55(dumps)
       20    0.000    0.000    0.001    0.000 rpc.py:556(__getattr__)
        1    0.000    0.000    0.001    0.001 rpc.py:574(__getmethods)
       20    0.000    0.000    0.000    0.000 rpc.py:598(__init__)
       20    0.000    0.000    0.050    0.002 rpc.py:603(__call__)
       20    0.000    0.000    0.051    0.003 run.py:340(write)
        1   20.722   20.722   20.773   20.773 test.py:3(foo)
       42    0.000    0.000    0.000    0.000 threading.py:1226(current_thread)
       21    0.000    0.000    0.000    0.000 threading.py:215(__init__)
       21    0.000    0.000    0.047    0.002 threading.py:263(wait)
       21    0.000    0.000    0.000    0.000 threading.py:74(RLock)
       21    0.000    0.000    0.000    0.000 {built-in method _struct.pack}
       21    0.000    0.000    0.000    0.000 {built-in method _thread.allocate_lock}
       42    0.000    0.000    0.000    0.000 {built-in method _thread.get_ident}
        1    0.000    0.000   20.773   20.773 {built-in method builtins.exec}
       42    0.000    0.000    0.000    0.000 {built-in method builtins.isinstance}
       63    0.000    0.000    0.000    0.000 {built-in method builtins.len}
       10    0.000    0.000    0.051    0.005 {built-in method builtins.print}
       21    0.000    0.000    0.000    0.000 {built-in method select.select}
       21    0.000    0.000    0.000    0.000 {method '_acquire_restore' of '_thread.RLock' objects}
       21    0.000    0.000    0.000    0.000 {method '_is_owned' of '_thread.RLock' objects}
       21    0.000    0.000    0.000    0.000 {method '_release_save' of '_thread.RLock' objects}
       21    0.000    0.000    0.000    0.000 {method 'acquire' of '_thread.RLock' objects}
       42    0.047    0.001    0.047    0.001 {method 'acquire' of '_thread.lock' objects}
       21    0.000    0.000    0.000    0.000 {method 'append' of 'collections.deque' objects}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
       21    0.000    0.000    0.000    0.000 {method 'dump' of '_pickle.Pickler' objects}
       20    0.000    0.000    0.000    0.000 {method 'get' of 'dict' objects}
       21    0.000    0.000    0.000    0.000 {method 'getvalue' of '_io.BytesIO' objects}
       21    0.000    0.000    0.000    0.000 {method 'release' of '_thread.RLock' objects}
       21    0.001    0.000    0.001    0.000 {method 'send' of '_socket.socket' objects}

Je m'attendais à ce qu'il me montre quelles parties de mon code prenaient le plus de temps, par exemple pour montrer que a = i**i Prenait le plus de temps à calculer, mais tout ce que je peux tirer de ce qu'il m'a dit, c'est que la fonction foo() a pris le plus de temps, mais ce qui a pris le plus de temps à l'intérieur de cette fonction, je n'ai aucune idée des données.

De plus, lorsque j'implémente cela dans mon code réel, cela fait la même chose. Tout est dans les fonctions, et cela me dit seulement quelles fonctions prennent le plus de temps plutôt que ce qui prend si longtemps dans la fonction.

Voici donc mes principales questions:

  1. Comment puis-je voir ce qui, à l'intérieur de la fonction, rend le code si long (devrais-je même utiliser cProfile?)

  2. Quelle est la meilleure façon d'optimiser mon code une fois que je sais ce qui utilise le plus de CPU

Remarque: Mon RAM et le disque etc. sont tout à fait corrects, c'est juste le CPU qui est au maximum (12% CPU car il ne fonctionne que sur un seul cœur)

18
Ruler Of The World

Comment puis-je voir ce qui, à l'intérieur de la fonction, rend le code si long (devrais-je même utiliser cProfile?)

Oui, vous pouvez utiliser cProfile mais la façon dont vous avez posé la question me fait me demander si line_profiler (module tiers, vous devez l'installer) ne serait pas un meilleur outil.

J'utilise les liaisons IPython/Jupyter de ce package lorsque je veux profiler une fonction:

%load_ext line_profiler

Pour réellement profiler une fonction:

%lprun -f foo foo()
#             ^^^^^---- this call will be profiled
#         ^^^-----------function to profile

Ce qui produit cette sortie:

Timer unit: 5.58547e-07 s

Total time: 17.1189 s
File: <ipython-input-1-21b5a5f52f66>
Function: foo at line 1

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
     1                                           def foo():
     2     10001        31906      3.2      0.1      for i in range(10000):
     3     10000     30534065   3053.4     99.6          a = i**i
     4     10000        75998      7.6      0.2          if i % 1000 == 0:
     5        10         6953    695.3      0.0              print(i)

Cela comprend plusieurs choses qui pourraient être intéressantes. Par exemple 99.6% du temps est passé dans le i**i ligne.

  1. Quelle est la meilleure façon d'optimiser mon code une fois que je sais ce qui utilise le plus de CPU

Ça dépend. Parfois, vous devez utiliser différentes fonctions/infrastructures de données/algorithmes - parfois, vous ne pouvez rien faire. Mais au moins, vous savez où se trouve votre goulot d'étranglement et vous pouvez estimer l'impact d'un changement au goulot d'étranglement ou ailleurs.

18
MSeifert

Comme vous l'avez remarqué dans le journal de profilage, la résolution maximale cProfile est la fonction.

Alors:

  • si votre fonction est petite, vous pourrez peut-être déterminer quelle partie prend du temps (bien que ce soit parfois difficile avec des appels intégrés comme in)
  • si votre fonction est importante, il est peut-être temps de la réduire à des fonctions plus petites, qui deviennent "profilables", mais la surcharge des appels de fonction peut ralentir les choses
2