Une mémoïsation qui ne mémoïse rien
Exercice · niveau 2 · informatique (MP2I/MPI), chapitre 17 — Programmation dynamique
Énoncé
Ce code paraît mémoïsé. Il ne l'est pas. Trouver la faute, et chiffrer ce qu'elle coûte.
let rec fib n =
let memo = Hashtbl.create 97 in
let rec aux k =
if k < 2 then k
else match Hashtbl.find_opt memo k with
| Some v -> v
| None ->
let v = fib (k - 1) + fib (k - 2) in (* <-- ici *)
Hashtbl.add memo k v;
v
in
aux nCorrigé
La faute tient en trois lettres : le corps appelle fib au lieu de aux. Or fib recrée une table vide à chaque appel. Chaque sous-appel repart donc d'une mémoire neuve, et aucune valeur n'est jamais retrouvée : la table est écrite, jamais lue.
Mesure (compteur d'appels à aux) :
n=10 fib= 55 appels = 177 (naif : 177)
n=20 fib= 6765 appels = 21891 (naif : 21891)
n=25 fib=75025 appels = 242785 (naif : 242785)
Exactement le nombre d'appels de la version naïve — la mémoïsation coûte le prix d'une table de hachage par appel, et ne rapporte rien. Le programme est même plus lent que le naïf.
Comment on l'attrape. Pas en lisant : en mesurant. Une mémoïsation correcte doit faire chuter le nombre d'appels d'un ordre de grandeur ; un compteur temporaire, ou un simple chronomètre sur , distingue les deux versions en une seconde. C'est le point du chapitre chap:discipline : un test qui n'observe que la valeur de retour ne voit pas cette faute, puisque le résultat est juste.
La correction est d'écrire aux (k - 1) + aux (k - 2), et l'on peut alors retirer le rec sur fib : le compilateur signale alors la faute tout seul si elle revient. Retirer un rec inutile n'est pas de la coquetterie : c'est transformer une faute silencieuse en erreur de compilation.
Les autres exercices de ce chapitre Le cours du chapitre
Un blocage sur cet exercice ? Le tuteur d'Adloun guide par questions, sans donner la réponse.