Exercice 01 : Profilage et Optimisation d'un Calcul de Fibonacci
Objectif
Cet exercice a pour but de vous faire utiliser cProfile et pstats pour profiler une fonction inefficace, identifier le goulot d'étranglement, l'optimiser, et vérifier le gain de performance en profilant à nouveau.
Contexte
La suite de Fibonacci est un exemple classique en informatique. Une implémentation récursive naïve est très simple à écrire, mais terriblement inefficace car elle recalcule les mêmes valeurs des milliers de fois.
fib(n) = fib(n-1) + fib(n-2)
Par exemple, pour calculer fib(5), on calcule fib(4) et fib(3). Mais pour fib(4), on recalcule fib(3) et fib(2). fib(3) est donc calculé deux fois. Ce problème s'aggrave de manière exponentielle.
Vous allez profiler cette fonction, puis l'optimiser en utilisant la mémoïsation (une forme de cache), et comparer les résultats.
Énoncé
Partie 1 : Profilage de la version inefficace
-
Créez un nouveau fichier Python nommé
profile_fib.py. -
Écrivez la fonction Fibonacci récursive naïve.
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2) -
Écrivez une fonction principale
mainqui appellefib(35). Le nombre 35 est assez grand pour que la fonction prenne quelques secondes.def main():
result = fib(35)
print(f"Fib(35) = {result}") -
Profilez le script depuis la ligne de commande.
- Exécutez la commande :
python -m cProfile -o fib_stats.prof profile_fib.py - Cela va prendre un certain temps.
- Exécutez la commande :
-
Analysez les résultats avec
pstats.- Lancez
pstats:python -m pstats fib_stats.prof - Triez par
ncalls(nombre d'appels) et affichez les 5 premières lignes (sort ncalls,stats 5). - Triez par
tottime(temps total) et affichez les 5 premières lignes (sort tottime,stats 5). - Notez le nombre total d'appels à la fonction
fibet le temps total d'exécution.
- Lancez
Partie 2 : Optimisation et nouveau profilage
-
Optimisez la fonction
fibavec la mémoïsation.- La mémoïsation consiste à stocker les résultats des appels de fonction dans un cache (un dictionnaire) pour éviter de les recalculer.
- Le module
functoolsfournit un décorateur parfait pour cela :@functools.lru_cache.
-
Modifiez votre fichier
profile_fib.py.- Importez
functools. - Ajoutez le décorateur
@functools.lru_cache(maxsize=None)juste au-dessus de votre fonctionfib.
import functools
@functools.lru_cache(maxsize=None)
def fib(n):
# ... même code qu'avant - Importez
-
Profilez la version optimisée.
- Exécutez à nouveau le profilage, mais en sauvegardant dans un autre fichier :
python -m cProfile -o fib_optimized_stats.prof profile_fib.py - Notez que l'exécution est maintenant quasi instantanée.
- Exécutez à nouveau le profilage, mais en sauvegardant dans un autre fichier :
-
Analysez les nouveaux résultats.
- Lancez
pstatssur le nouveau fichier :python -m pstats fib_optimized_stats.prof - Triez par
ncallsettottime. - Comparez le nombre d'appels à
fibet le temps d'exécution avec la version précédente.
- Lancez
Résultat Attendu
Analyse de la version 1 (naïve)
sort ncalls/stats 5devrait montrer un nombre d'appels àfibextraordinairement élevé (plusieurs dizaines de millions).ncalls
29860703 ... profile_fib.py:4(fib)sort tottime/stats 5devrait montrer que la quasi-totalité du temps est passée dans la fonctionfib.tottime
3.512 ... profile_fib.py:4(fib)
Analyse de la version 2 (optimisée)
sort ncalls/stats 5devrait montrer que la fonctionfibn'est appelée qu'environ 36 fois (une fois pour chaque nombre de 0 à 35).ncalls
36 ... profile_fib.py:7(fib)sort tottime/stats 5devrait montrer un temps d'exécution négligeable (proche de 0.000s).
Cette comparaison démontre de manière spectaculaire comment le profilage permet d'identifier une fonction inefficace et de valider l'impact d'une optimisation.
Cliquez ici pour voir un exemple de code de solution
# profile_fib.py
import functools
# Pour tester la version non optimisée, commentez la ligne @functools.lru_cache
@functools.lru_cache(maxsize=None)
def fib(n: int) -> int:
"""
Calcule le n-ième nombre de Fibonacci.
Version optimisée avec mémoïsation via lru_cache.
"""
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
def main():
"""Fonction principale pour lancer le calcul."""
print("Calcul de Fib(35)...")
result = fib(35)
print(f"Fib(35) = {result}")
if __name__ == "__main__":
main()
Instructions d'exécution (rappel)
-
Version non optimisée :
- Commentez la ligne
@functools.lru_cache(maxsize=None). python -m cProfile -o fib_stats.prof profile_fib.pypython -m pstats fib_stats.prof- Dans pstats, tapez
sort ncallspuisstats 5.
- Commentez la ligne
-
Version optimisée :
- Décommentez la ligne
@functools.lru_cache(maxsize=None). python -m cProfile -o fib_optimized_stats.prof profile_fib.pypython -m pstats fib_optimized_stats.prof- Dans pstats, tapez
sort ncallspuisstats 5.
- Décommentez la ligne