Files
2026_TD1_DEV51_Qualite_Algo/notes.md
T
2026-10-07 12:21:17 +02:00

17 KiB

Lancement du programme après la compilation avec gcc -O0 analyseur.c -o analyseur

Lignes analysées      : 21746
Mots au total         : 250000
Mots distincts        : 5866
Longueur moyenne      : 6.712 (écart-type 1.873)
Bigramme le + courant : "me" (17607 fois, souvent suivi de 'n')

Top 10 :
   1. rane             287
   2. fasi             241
   3. tata             219
   4. mopo             214
   5. cofi             180
   6. vesi             179
   7. laganomo         177
   8. boto             172
   9. nopebo           169
  10. rotion           165

Empreinte du rapport  : 1d53de903efd2b7c

Et ensuite la commande time sur l'exécutable avec time ./analyseur

Lignes analysées      : 21746
Mots au total         : 250000
Mots distincts        : 5866
Longueur moyenne      : 6.712 (écart-type 1.873)
Bigramme le + courant : "me" (17607 fois, souvent suivi de 'n')

Top 10 :
   1. rane             287
   2. fasi             241
   3. tata             219
   4. mopo             214
   5. cofi             180
   6. vesi             179
   7. laganomo         177
   8. boto             172
   9. nopebo           169
  10. rotion           165

Empreinte du rapport  : 1d53de903efd2b7c

real    0m2.858s
user    0m2.821s
sys     0m0.013s

On recompile avec le flag -pg : gcc -O0 -pg analyseur.c -o analyseur Et on relance time dessus

Lignes analysées      : 21746
Mots au total         : 250000
Mots distincts        : 5866
Longueur moyenne      : 6.712 (écart-type 1.873)
Bigramme le + courant : "me" (17607 fois, souvent suivi de 'n')

Top 10 :
   1. rane             287
   2. fasi             241
   3. tata             219
   4. mopo             214
   5. cofi             180
   6. vesi             179
   7. laganomo         177
   8. boto             172
   9. nopebo           169
  10. rotion           165

Empreinte du rapport  : 1d53de903efd2b7c

real    0m6.354s
user    0m6.295s
sys     0m0.020s

On lance gprof : gprof -b ./analyseur gmon.out > profil_avant.txt

Optimisation de chercher_mot : recherche dichotomique

La recherche initiale parcourait toutes les entrées jusqu'à trouver le mot. Pour 150 182 recherches, elle effectuait 296 656 203 appels à egales().

Le tableau est maintenant maintenu dans l'ordre alphabétique pendant le comptage. chercher_mot utilise comparer() pour éliminer la moitié des entrées restantes à chaque étape : la recherche passe de O(n) à O(log n). Si le mot est absent, la fonction renvoie -(position d'insertion + 1) ; compter_mot retrouve cette position avec -indice - 1, décale les entrées et insère le nouveau mot. Cette insertion reste en O(n), mais elle n'a lieu qu'à la première apparition de chacun des 5 866 mots distincts. Le tableau existant est réutilisé, sans allocation supplémentaire. Le tri final par fréquence produit le même classement.

Résultats du nouveau profil, conservé dans out/profil_apres.txt :

Mesure Avant Après
Appels de chercher_mot 150 182 150 182
Appels à egales() depuis chercher_mot 296 656 203 0
Comparaisons de chaînes dans chercher_mot 296 656 203 1 718 447
Appels à egales() dans tout le programme 308 701 874 12 045 671
Temps réel sans profiling (gcc -O0) 2,80 s 1,45 s

Les recherches effectuent environ 173 fois moins de comparaisons. Les appels restants à egales() viennent de est_mot_vide. Les temps ci-dessus correspondent à une mesure avant/après sur la même machine pendant cette modification : environ 48 % de temps en moins (accélération d'environ 1,93). Ce sont des mesures ponctuelles.

Validation : compilation avec -Wall -Wextra -Wpedantic, rapport et sortie console identiques octet par octet à la version initiale, avec et sans -pg. L'empreinte reste 1d53de903efd2b7c. Un test temporaire sous AddressSanitizer et UndefinedBehaviorSanitizer a également vérifié les recherches dans un tableau vide, les mots présents ou absents, les insertions au début/milieu/fin, les doublons et les mots de 31 caractères, puis comparé 10 000 comptages à une référence linéaire.

Pour reproduire le profil sans écraser les fichiers du profil initial :

mkdir -p out/apres
gcc -O0 -pg analyseur.c -o out/apres/analyseur
(cd out/apres && ./analyseur && gprof -b ./analyseur gmon.out > ../profil_apres.txt)
Lignes analysées      : 21746
Mots au total         : 250000
Mots distincts        : 5866
Longueur moyenne      : 6.712 (écart-type 1.873)
Bigramme le + courant : "me" (17607 fois, souvent suivi de 'n')

Top 10 :
   1. rane             287
   2. fasi             241
   3. tata             219
   4. mopo             214
   5. cofi             180
   6. vesi             179
   7. laganomo         177
   8. boto             172
   9. nopebo           169
  10. rotion           165

Empreinte du rapport  : 1d53de903efd2b7c

real    0m1.986s
user    0m1.934s
sys     0m0.011s

Optimisation des appels à longueur_moyenne

longueur_moyenne parcourt les mots une fois par appel. Le coût excessif venait de ecart_type_longueur, qui recalculait cette même moyenne à chaque itération, soit 5 866 fois. Avec l'appel de main, cela représentait 5 867 calculs de moyenne et 34 415 822 appels à longueur().

main calcule désormais la moyenne une seule fois et la transmet en paramètre à ecart_type_longueur. La boucle utilise directement cette valeur. Le calcul des statistiques passe ainsi de O(n²) à O(n), sans allocation supplémentaire ni modification de la formule de variance.

Profil enregistré dans out/profil_apres_moyenne.txt. La colonne « Avant » correspond à la version déjà optimisée pour chercher_mot, dont le profil reste dans out/profil_apres.txt.

Mesure Avant Après
Appels à longueur_moyenne 5 867 1
Appels à longueur() depuis longueur_moyenne 34 415 822 5 866
Appels à longueur() dans tout le programme 36 009 819 1 599 863
Temps réel sans profiling (gcc -O0) 1,46 s 0,88 s

Cette mesure ponctuelle sur la même machine donne environ 40 % de temps en moins, soit une accélération d'environ 1,66 par rapport à l'étape précédente.

Validation : compilation avec -Wall -Wextra -Wpedantic sans avertissement, puis comparaison du rapport et de la sortie console avant/après, avec et sans -pg. Ils restent identiques octet par octet, y compris la moyenne (6.712) et l'écart-type (1.873). L'empreinte reste 1d53de903efd2b7c.

Commandes pour reproduire ce profil :

mkdir -p out/apres_moyenne
gcc -O0 -pg analyseur.c -o out/apres_moyenne/analyseur
(cd out/apres_moyenne && ./analyseur && gprof -b ./analyseur gmon.out > ../profil_apres_moyenne.txt)
[rabot@salle235-06 out]$ time ./analyseur 
Lignes analysées      : 21746
Mots au total         : 250000
Mots distincts        : 5866
Longueur moyenne      : 6.712 (écart-type 1.873)
Bigramme le + courant : "me" (17607 fois, souvent suivi de 'n')

Top 10 :
   1. rane             287
   2. fasi             241
   3. tata             219
   4. mopo             214
   5. cofi             180
   6. vesi             179
   7. laganomo         177
   8. boto             172
   9. nopebo           169
  10. rotion           165

Empreinte du rapport  : 1d53de903efd2b7c

real    0m1.251s
user    0m1.214s
sys     0m0.004s

Remplacement de la dichotomie par une table de hachage

Un index de 40 001 cases associe chaque mot à son entrée dans le tableau. hacher_mot calcule un entier à partir des caractères du mot ; le modulo donne la première case à examiner. Chaque case contient l'indice de l'entrée + 1, ou zéro si elle est libre.

En cas de collision, chercher_mot vérifie le mot avec egales() puis avance à la case suivante, en revenant au début si nécessaire. Deux mots ayant le même hachage restent donc bien distincts. Les nouveaux mots sont ajoutés à la fin du tableau, sans décaler les entrées existantes.

La recherche et l'insertion sont en O(1) en moyenne pour ces mots de longueur bornée, avec un pire cas en O(n) en cas de nombreuses collisions. La limite de 20 000 mots distincts est vérifiée avant chaque nouvelle insertion ; avec 40 001 cases, l'index reste rempli à moins de 50 %.

Ce choix ajoute 40 001 * sizeof(int) octets, soit environ 156 Kio sur cette machine. L'index est libéré après le comptage, avant le tri final qui déplace les entrées. L'empreinte du rapport est conservée, mais cette version utilise davantage de mémoire pendant le comptage.

Profil enregistré dans out/profil_apres_hachage.txt. La comparaison porte sur la version précédente avec dichotomie et moyenne calculée une seule fois, conservée dans out/profil_apres_moyenne.txt.

Mesure Dichotomie Hachage
Appels de chercher_mot 150 182 150 182
Comparaisons de chaînes dans chercher_mot 1 718 447 (comparer) 153 101 (egales)
Temps réel médian sans profiling (gcc -O0, 5 exécutions par version) 0,888 s 0,831 s

Les recherches font environ 11,2 fois moins de comparaisons. Le temps total baisse d'environ 6,4 % : le gain reste limité car la normalisation et le tri occupent désormais l'essentiel du temps. L'ordre d'arrivée des mots remplace l'ordre alphabétique pendant le comptage ; le tri final effectue donc un nombre différent de comparaisons, tout en produisant le même classement.

Les cinq mesures, réalisées en alternant les deux versions sur la même machine, sont respectivement : 0,883 / 0,892 / 0,888 / 0,883 / 0,888 s avant, et 0,831 / 0,830 / 0,831 / 0,832 / 0,829 s après.

Validation : compilation sans avertissement avec -Wall -Wextra -Wpedantic, rapport et sortie console identiques octet par octet sur les dix exécutions et avec -pg. L'empreinte reste 1d53de903efd2b7c. Des tests temporaires sous AddressSanitizer et UndefinedBehaviorSanitizer ont vérifié les collisions avec retour à la première case, les doublons, les mots de 0/1/31 caractères, 10 000 comptages comparés à une référence linéaire, les 20 000 entrées possibles et le refus d'un dépassement. Le programme complet a également été exécuté avec ces deux outils, sans erreur et avec une sortie identique.

Commandes pour reproduire ce profil :

mkdir -p out/apres_hachage
gcc -O0 -pg analyseur.c -o out/apres_hachage/analyseur
(cd out/apres_hachage && ./analyseur && gprof -b ./analyseur gmon.out > ../profil_apres_hachage.txt)

Longueurs mémorisées et suppression des parcours répétés

Le champ taille de Entree mémorise la longueur du mot à son insertion. La valeur vient directement de k, déjà calculé dans compter_mots_ligne : aucun parcours supplémentaire n'est nécessaire. La moyenne et la variance lisent désormais ce champ. Le tri déplace l'entrée entière, donc la longueur reste associée au bon mot. Pour un mot tronqué, la taille enregistrée est celle du mot conservé, soit au maximum 31 caractères.

Deux autres changements éliminent les recalculs sur les chaînes longues :

  • normaliser_ligne s'arrête directement sur ligne[i] == '\0'. Le parcours d'une ligne de L caractères passe de O(L²) à O(L).
  • ajouter reçoit une position de fin, qu'il avance pendant la copie des caractères ajoutés. Cette position est conservée lors de la construction du rapport et réinitialisée pour chaque mot du vocabulaire. Le préfixe déjà construit n'est plus parcouru à chaque ajout.

La fonction longueur, devenue inutilisée, a été supprimée. Le champ supplémentaire fait passer sizeof(Entree) de 36 à 40 octets sur cette machine : le tableau alloué pour 20 000 entrées prend 80 000 octets supplémentaires, soit environ 78 Kio.

Profil enregistré dans out/profil_apres_longueurs.txt. La version précédente, avec la table de hachage, est décrite dans out/profil_apres_hachage.txt.

Mesure Avant Après
Appels à longueur() depuis les statistiques 11 732 0
Appels à longueur() depuis normaliser_ligne 1 561 330 0
Appels à longueur() depuis ajouter 26 801 0
Appels à longueur() au total 1 599 863 0
Temps réel médian sans profiling (gcc -O0, 5 exécutions par version) 0,832 s 0,362 s

Le temps total baisse de 56,5 %, soit une accélération d'environ 2,30. Cette mesure porte sur l'ensemble des trois changements, et ne doit pas être attribuée uniquement au stockage de la longueur des mots.

Les cinq mesures, réalisées en alternant les deux versions sur la même machine, sont respectivement : 0,835 / 0,838 / 0,830 / 0,832 / 0,829 s avant, et 0,362 / 0,369 / 0,366 / 0,362 / 0,361 s après.

Validation : compilation sans avertissement avec -Wall -Wextra -Wpedantic, rapport et sortie console identiques octet par octet sur les dix exécutions, avec -pg et sous AddressSanitizer et UndefinedBehaviorSanitizer. L'empreinte reste 1d53de903efd2b7c, la moyenne 6.712 et l'écart-type 1.873. Des tests temporaires ont également vérifié les ajouts vides et successifs jusqu'à la limite d'un tampon, la normalisation, les longueurs avant/après tri, les statistiques, le rapport vide et les mots tronqués.

Commandes pour reproduire ce profil :

mkdir -p out/apres_longueurs
gcc -O0 -pg analyseur.c -o out/apres_longueurs/analyseur
(cd out/apres_longueurs && ./analyseur && gprof -b ./analyseur gmon.out > ../profil_apres_longueurs.txt)
[rabot@salle235-06 out]$ time ./analyseur 
Lignes analysées      : 21746
Mots au total         : 250000
Mots distincts        : 5866
Longueur moyenne      : 6.712 (écart-type 1.873)
Bigramme le + courant : "me" (17607 fois, souvent suivi de 'n')

Top 10 :
   1. rane             287
   2. fasi             241
   3. tata             219
   4. mopo             214
   5. cofi             180
   6. vesi             179
   7. laganomo         177
   8. boto             172
   9. nopebo           169
  10. rotion           165

Empreinte du rapport  : 1d53de903efd2b7c

real    0m0.751s
user    0m0.703s
sys     0m0.004s

Remplacement du tri à bulles par un tri par tas

Le tri à bulles effectuait systématiquement n(n - 1)/2 comparaisons, soit 17 202 045 appels à doit_echanger pour 5 866 mots distincts.

trier_entrees utilise maintenant un tri par tas. La racine du tas contient l'entrée qui doit apparaître le plus tard dans le classement. Elle est échangée avec la dernière entrée de la partie non triée, puis retablir_tas restaure cette propriété sur les entrées restantes. La construction du tas coûte O(n), puis les extractions coûtent O(log n) chacune : le tri complet passe de O(n²) à O(n log n), y compris dans le pire cas.

Le tri travaille directement dans le tableau, avec O(1) de mémoire supplémentaire. doit_echanger reste inchangée : fréquence décroissante, puis ordre alphabétique croissant. Chaque échange déplace l'entrée entière, y compris la longueur mémorisée.

Profil enregistré dans out/profil_apres_tri.txt. La comparaison porte sur la version précédente, décrite dans out/profil_apres_longueurs.txt.

Mesure Tri à bulles Tri par tas
Appels à doit_echanger pendant le tri 17 202 045 129 602
Appels à comparer pour départager les fréquences égales 12 172 087 32 813
Temps réel médian du programme sans profiling (gcc -O0, 5 exécutions par version) 0,367 s 0,113 s

Le tri effectue environ 133 fois moins de comparaisons d'entrées. Le temps total baisse de 69,3 %, soit une accélération d'environ 3,25. Les cinq mesures, réalisées en alternant les deux versions sur la même machine, sont respectivement : 0,365 / 0,363 / 0,372 / 0,383 / 0,367 s avant, et 0,112 / 0,113 / 0,115 / 0,113 / 0,112 s après.

Validation : compilation sans avertissement avec -Wall -Wextra -Wpedantic, rapport et sortie console identiques octet par octet sur les dix exécutions, avec -pg et sous AddressSanitizer et UndefinedBehaviorSanitizer. L'empreinte reste 1d53de903efd2b7c. Des tests temporaires ont comparé le résultat à qsort sur des tableaux de 0 à 20 000 entrées, aléatoires, déjà triés ou inversés. Ils vérifient aussi les fréquences égales, les doublons, les préfixes, les valeurs extrêmes de fréquence et la conservation des longueurs des mots.

Commandes pour reproduire ce profil :

mkdir -p out/apres_tri
gcc -O0 -pg analyseur.c -o out/apres_tri/analyseur
(cd out/apres_tri && ./analyseur && gprof -b ./analyseur gmon.out > ../profil_apres_tri.txt)
[rabot@salle235-06 out]$ time ./analyseur 
Lignes analysées      : 21746
Mots au total         : 250000
Mots distincts        : 5866
Longueur moyenne      : 6.712 (écart-type 1.873)
Bigramme le + courant : "me" (17607 fois, souvent suivi de 'n')

Top 10 :
   1. rane             287
   2. fasi             241
   3. tata             219
   4. mopo             214
   5. cofi             180
   6. vesi             179
   7. laganomo         177
   8. boto             172
   9. nopebo           169
  10. rotion           165

Empreinte du rapport  : 1d53de903efd2b7c

real    0m0.354s
user    0m0.314s
sys     0m0.008s