From 2878461ebd6abd3be14c67a0eef2ffdc8ed854cd Mon Sep 17 00:00:00 2001 From: rabot Date: Wed, 7 Oct 2026 12:50:25 +0200 Subject: [PATCH] Compte rendu final --- COMPTE_RENDU.md | 305 ++++++++---------------------------------------- 1 file changed, 46 insertions(+), 259 deletions(-) diff --git a/COMPTE_RENDU.md b/COMPTE_RENDU.md index 2985b95..d7432ed 100644 --- a/COMPTE_RENDU.md +++ b/COMPTE_RENDU.md @@ -1,281 +1,68 @@ -# Compte rendu — Profiling et optimisation de l'analyseur de mots +# Compte rendu — Optimisation de l'analyseur de mots **R5.A.04 — Qualité algorithmique** -Les optimisations ont ramené le temps réel médian du programme de **2,81 s -à 0,11 s**, sur une comparaison de cinq exécutions. L'empreinte du rapport est toujours **`1d53de903efd2b7c`**. +**Adrien RABOT — Théo ANASTASIO** -## 1. Objectif et méthode de mesure +## 1. Objectif et méthode -L'objectif est de réduire le temps d'exécution en conservant les résultats. -Les mesures et les choix décrits ci-dessous proviennent de [notes.md](notes.md) -et des profils enregistrés dans `out/`. Une campagne complémentaire a été -réalisée pour mesurer le temps global et le pic mémoire des versions -initiale et finale dans les mêmes conditions. +Réduire le temps d'exécution sans modifier les résultats, sur un texte fixe +contenant 250 000 mots et 5 866 mots distincts retenus. -Deux compilations sont utilisées : +Les temps sont mesurés avec `gcc -O0`, sans `-pg`. Le profil est obtenu +séparément avec `gcc -O0 -pg` et `gprof`, car l'instrumentation ralentit le programme. -- `gcc -O0` pour mesurer le temps réel d'exécution ; -- `gcc -O0 -pg` pour obtenir le profil plat et le graphe d'appels avec `gprof`. +## 2. Analyse et pistes prioritaires -La première mesure consignée dans les notes donne **2,858 s sans `-pg`**, -contre **6,354 s avec `-pg`**. L'instrumentation a donc un coût important. -Les gains de performance sont calculés entre exécutables compilés sans -`-pg`, en conservant `-O0` pour observer l'effet des changements algorithmiques. -Les captures intermédiaires dont les options de compilation ne sont pas -précisées ne servent pas au calcul des gains. +Le profil initial conduit aux priorités suivantes, classées par gain attendu : -## 2. Analyse du profil initial +1. **Accélérer la recherche des mots.** La recherche linéaire provoque + 296 656 203 appels à `egales`. Une recherche dichotomique ou une table + de hachage permet de réduire ces comparaisons. +2. **Calculer la moyenne une seule fois.** `longueur_moyenne` est appelée + 5 867 fois, car la variance recalcule la même moyenne pour chaque mot. +3. **Supprimer les parcours répétés des chaînes.** `longueur` est appelée + 36 009 819 fois au total. Mémoriser les longueurs et les positions de fin + évite de reparcourir les mots, les lignes et le rapport. +4. **Remplacer le tri à bulles.** Ses 17 202 045 appels à `doit_echanger` + justifient un tri en O(n log n). -Le [profil initial](out/profil_avant.txt) fait apparaître les coûts suivants. -Le temps propre correspond au temps passé dans la fonction elle-même, -hors fonctions qu'elle appelle. +## 3. Optimisations et gains mesurés -| Fonction | Nombre d'appels | Part du temps propre | -| --- | ---: | ---: | -| `longueur` | 36 009 819 | 36,36 % | -| `egales` | 308 701 874 | 26,32 % | -| `chercher_mot` | 150 182 | 18,18 % | -| `trier_entrees` | 1 | 5,26 % | -| `longueur_moyenne` | 5 867 | 3,35 % | +Chaque gain est relatif à la version précédente. Les deux premières étapes +reposent sur une mesure ponctuelle ; les suivantes utilisent la médiane de +cinq exécutions par version. -Le graphe d'appels permet d'expliquer ces résultats : +| Optimisation | Modification | Avant | Après | Temps gagné | +| --- | --- | ---: | ---: | ---: | +| Recherche dichotomique | Tableau maintenu trié, recherche en O(log n) | 2,80 s | 1,45 s | ≈ 48 % | +| Moyenne | Un seul calcul, transmis au calcul de variance | 1,46 s | 0,88 s | ≈ 40 % | +| Table de hachage | Recherche et insertion en O(1) en moyenne | 0,888 s | 0,831 s | 6,4 % | +| Longueurs et chaînes | Champ `taille`, arrêt sur `\0`, position de fin conservée | 0,832 s | 0,362 s | 56,5 % | +| Tri par tas | Tri en O(n log n), sans tableau supplémentaire | 0,367 s | 0,113 s | 69,3 % | -- **Recherche des mots.** `chercher_mot` parcourt les entrées une à une. - Ses 150 182 recherches provoquent 296 656 203 appels à `egales`. - Le problème vient du nombre de comparaisons effectuées. -- **Statistiques.** `ecart_type_longueur` recalcule la même moyenne pour - chaque mot distinct. Avec l'appel effectué dans `main`, cela représente - 5 867 calculs de moyenne et 34 415 822 appels à `longueur` depuis - `longueur_moyenne`. -- **Parcours des chaînes.** `normaliser_ligne` recalcule la longueur de la - ligne à chaque caractère. `ajouter` recherche également la fin de la - destination à chaque ajout, notamment lorsque le rapport grandit. -- **Tri.** Le tri à bulles effectue systématiquement `n(n - 1)/2` - comparaisons, soit 17 202 045 appels à `doit_echanger` pour 5 866 entrées. +La version finale utilise le hachage à la place de la dichotomie. Son gain +supplémentaire reste limité car la recherche était déjà efficace. -Le nombre d'appels doit être interprété avec le travail effectué par appel : -parcourir un rapport déjà long coûte davantage que parcourir un mot court. -Les modifications sont donc évaluées par un nouveau chronométrage, en -complément du profil. +Tous les appels à `longueur` sont supprimés. Le tri ne fait plus que +**129 602 appels à `doit_echanger`**, soit environ **133 fois moins**. +Le classement reste par fréquence décroissante, puis par ordre alphabétique. -## 3. Pistes d'amélioration, classées par gain attendu +## 4. Bilan et validation -Dans les complexités indiquées, `n` désigne le nombre de mots distincts -et `L` la longueur d'une ligne. Les mots conservés sont limités à 31 caractères. +Une comparaison directe des versions initiale et finale donne les médianes +suivantes, sur cinq exécutions par version dans les mêmes conditions : -| Priorité | Piste | Gain attendu et justification | -| ---: | --- | --- | -| 1 | Remplacer la recherche linéaire par une recherche dichotomique, puis étudier le hachage | Très élevé face à la version initiale : éviter des centaines de millions de comparaisons. | -| 2 | Calculer la moyenne une seule fois | Très élevé : supprimer un calcul identique répété pour chaque entrée et passer de O(n²) à O(n). | -| 3 | Éviter les recalculs de longueur des lignes et des chaînes construites | Élevé : supprimer les parcours répétés de chaînes longues. | -| 4 | Remplacer le tri à bulles par un tri en O(n log n) | Élevé une fois les autres coûts réduits : diminuer les 17 millions de comparaisons du tri. | -| 5 | Mémoriser la longueur des mots | Plus limité isolément : après la correction de la moyenne, les statistiques ne font plus que 11 732 calculs de longueur sur des mots courts. | - -La priorité porte ainsi sur la réduction du nombre de calculs. Les fonctions -`egales` et `doit_echanger` peuvent conserver leur comportement pendant que -les algorithmes qui les utilisent changent. - -## 4. Optimisations réalisées - -### 4.1. Recherche dichotomique - -Une première version maintient le tableau dans l'ordre alphabétique pendant -le comptage. La recherche élimine la moitié des candidats à chaque étape : -elle passe de O(n) à O(log n). L'insertion d'un nouveau mot reste en O(n), -car elle nécessite de décaler des entrées, mais les occurrences suivantes -n'entraînent aucun déplacement. - -Le nombre de comparaisons de chaînes dans la recherche passe de -296 656 203 à 1 718 447, soit environ **173 fois moins**. Cette étape est -conservée dans l'historique ; la version finale utilise le hachage. - -### 4.2. Calcul unique de la moyenne - -`main` calcule la moyenne une seule fois, puis la transmet à -`ecart_type_longueur`. La boucle de calcul de la variance réutilise cette -valeur. Le nombre d'appels à `longueur_moyenne` passe de 5 867 à **1**, -et le calcul des statistiques passe de O(n²) à O(n), sans changer la formule. - -### 4.3. Table de hachage pour le comptage - -Un index de 40 001 cases associe chaque mot à son entrée dans le tableau. -Lors d'une collision, le programme compare les mots puis examine la case -suivante. Les nouveaux mots sont ajoutés en fin de tableau, sans décalage. -La limite de 20 000 mots distincts maintient l'index rempli à moins de 50 %. - -La recherche et l'insertion deviennent O(1) en moyenne pour ces mots de -longueur bornée, avec un pire cas en O(n) en cas de nombreuses collisions. -L'index est libéré après le comptage, avant le tri qui déplace les entrées. - -La recherche effectue désormais **153 101 comparaisons**, environ 11,2 fois -moins qu'avec la dichotomie. Le gain global est cependant de 6,4 % à cette -étape : la recherche était déjà optimisée et les autres traitements -représentaient l'essentiel du temps restant. - -### 4.4. Longueurs mémorisées et parcours des chaînes - -Trois changements suppriment les appels restants à `longueur` : - -- le champ `taille` de `Entree` reçoit la longueur déjà calculée pendant - le découpage du mot ; la moyenne et la variance lisent directement ce champ ; -- `normaliser_ligne` parcourt la ligne jusqu'à `ligne[i] == '\0'`, ce qui - ramène sa complexité de O(L²) à O(L) ; -- `ajouter` reçoit une position de fin qui avance pendant la copie des - nouveaux caractères. Le rapport et les mots du vocabulaire sont ainsi - construits sans reparcourir leur préfixe à chaque ajout. - -Les **1 599 863 appels restants à `longueur` disparaissent** et la fonction -est supprimée. Le gain mesuré concerne cet ensemble de changements ; il ne -peut pas être attribué au seul stockage de la longueur des mots. - -### 4.5. Tri par tas - -Le tri à bulles est remplacé par un tri par tas. Le tas place à sa racine -l'entrée qui doit apparaître le plus tard dans le classement. Cette entrée -est placée à la fin de la partie non triée, puis le tas est rétabli. - -Le tri est en **O(n log n), y compris dans le pire cas**, avec O(1) de mémoire -supplémentaire. `doit_echanger` reste inchangée : fréquence décroissante, -puis ordre alphabétique croissant. Les échanges déplacent les entrées -entières, avec leur longueur mémorisée. - -Les appels à `doit_echanger` passent de **17 202 045 à 129 602**, soit environ -133 fois moins. Les comparaisons alphabétiques passent de 12 172 087 à 32 813. - -Ces cinq étapes correspondent aux commits `be387e2`, `d3f1987`, `57aaec4`, -`1c4405c` et `57c9ce9`. - -## 5. Temps mesurés et gains obtenus - -### Mesures par étape issues des notes - -Chaque gain est relatif à la version immédiatement précédente. Les mesures -ont été refaites à chaque étape, ce qui explique les petits écarts entre -le temps « après » d'une ligne et le temps « avant » de la suivante. -Les pourcentages sont ceux consignés dans les notes ; les temps affichés -sont arrondis. - -| Modification | Avant | Après | Réduction du temps | Profil obtenu | -| --- | ---: | ---: | ---: | --- | -| Recherche dichotomique | 2,80 s | 1,45 s | ≈ 48 % | [profil_apres.txt](out/profil_apres.txt) | -| Moyenne calculée une seule fois | 1,46 s | 0,88 s | ≈ 40 % | [profil_apres_moyenne.txt](out/profil_apres_moyenne.txt) | -| Table de hachage | 0,888 s | 0,831 s | 6,4 % | [profil_apres_hachage.txt](out/profil_apres_hachage.txt) | -| Longueurs et parcours des chaînes | 0,832 s | 0,362 s | 56,5 % | [profil_apres_longueurs.txt](out/profil_apres_longueurs.txt) | -| Tri par tas | 0,367 s | 0,113 s | 69,3 % | [profil_apres_tri.txt](out/profil_apres_tri.txt) | - -Le profil final est donc `out/profil_apres_tri.txt`. Le fichier -`out/profil_apres.txt` correspond uniquement à la première optimisation. - -### Comparaison globale complémentaire - -Pour compléter les notes, la version initiale du commit `55320bd` et la -version finale ont été compilées avec GCC 16.2.1 sous Linux x86_64, avec -`-O0` et sans `-pg`. Cinq exécutions par version ont été réalisées en -alternance. `/usr/bin/time` a mesuré les durées et le maximum de mémoire -résidente (RSS). Les résultats bruts sont conservés dans -[mesures_bilan.csv](out/mesures_bilan.csv). - -| Mesure médiane sur cinq exécutions | Version initiale | Version finale | +| Mesure | Version initiale | Version finale | | --- | ---: | ---: | | Temps réel | 2,81 s | 0,11 s | -| Temps utilisateur | 2,80 s | 0,11 s | -| Pic mémoire résident | 6 664 Kio | 6 668 Kio | +| Pic mémoire résident (RSS) | 6 664 Kio | 6 668 Kio | -Le rapport des temps donne une accélération d'environ **25,5**, soit une -réduction d'environ **96,1 %**. Ces valeurs restent des mesures sur ce jeu -de données et cette machine ; les temps de cette campagne sont relevés -au centième de seconde. +Le programme est environ **25,5 fois plus rapide**, soit **96,1 % de temps +en moins** sur ce jeu de données. -## 6. Validation des résultats +Le rapport et la sortie console sont identiques octet par octet. +L'empreinte reste **`1d53de903efd2b7c`**. -Après chaque étape, le rapport et la sortie console ont été comparés à -la version précédente, octet par octet. La campagne complémentaire compare -également directement les versions initiale et finale. Les résultats -suivants sont conservés : - -| Résultat | Valeur commune aux versions initiale et finale | -| --- | --- | -| Lignes analysées | 21 746 | -| Mots au total | 250 000 | -| Mots distincts retenus | 5 866 | -| Longueur moyenne | 6,712 | -| Écart-type | 1,873 | -| Bigramme le plus fréquent | `me`, 17 607 fois, souvent suivi de `n` | -| Mot le plus fréquent | `rane`, 287 occurrences | -| Empreinte du rapport | **`1d53de903efd2b7c`** | - -Les compilations avec `-Wall -Wextra -Wpedantic` n'ont produit aucun -avertissement. Les validations consignées dans les notes comprennent -également des exécutions avec et sans `-pg`, ainsi que sous -AddressSanitizer et UndefinedBehaviorSanitizer, sans erreur détectée. - -Des tests temporaires ont couvert les collisions de hachage, les doublons, -les limites de capacité, les mots tronqués à 31 caractères et les ajouts -jusqu'à la limite d'un tampon. Le tri a été comparé à `qsort` sur des -tableaux de 0 à 20 000 entrées, aléatoires, déjà triés ou inversés, avec -des fréquences égales. La conservation des longueurs après tri a aussi -été vérifiée. - -## 7. Consommation mémoire et limites - -L'empreinte du **rapport** et l'empreinte **mémoire** sont deux mesures -différentes. La première est conservée, mais deux optimisations augmentent -la mémoire allouée sur cette machine : - -| Structure ajoutée ou agrandie | Mémoire supplémentaire | -| --- | ---: | -| Index de hachage : 40 001 entiers de 4 octets | 160 004 octets, soit environ 156 Kio | -| Champ `taille` : 4 octets pour chacune des 20 000 entrées allouées | 80 000 octets, soit environ 78 Kio | -| Total supplémentaire pendant le comptage | 240 004 octets, soit environ 234 Kio | - -L'index est libéré avant le tri, qui ne nécessite pas de tableau auxiliaire. -Les pics RSS mesurés sont proches et fluctuent : de 6 504 à 6 672 Kio pour -la version initiale, et de 6 652 à 6 672 Kio pour la version finale. Le RSS -mesure la mémoire résidente de l'ensemble du processus, et non la somme -des tailles demandées à `malloc` et `calloc`. - -Cette proximité des pics ne permet donc pas d'affirmer que la consommation -mémoire est strictement identique. Au sens de la mémoire allouée, la -contrainte de conservation mentionnée dans le [README](README.md) n'est -pas respectée par le hachage et la mémorisation des longueurs : ces choix -constituent un compromis entre temps d'exécution et mémoire. - -## 8. Analyse du profil final et suite possible - -Dans le profil final, `est_mot_vide` effectue encore **12 045 671 appels à -`egales`**, sur 12 198 772 appels au total. La recherche linéaire dans la -liste des mots vides constitue donc une prochaine piste d'optimisation, -par exemple avec une structure de recherche adaptée. Cette piste n'a pas -été mise en œuvre dans la version présentée. - -Le profil final ne cumule que 0,11 s de temps échantillonné, avec une -granularité de 0,01 s. Ses pourcentages doivent être interprétés avec -prudence : une ligne à `0,00 s` ne signifie pas que la fonction ne coûte -rien. Les nombres d'appels et les chronométrages répétés complètent cette -lecture. - -## 9. Commandes de reproduction - -Depuis la racine du dépôt, les commandes suivantes créent des exécutables -dans un dossier séparé et produisent un nouveau profil final : - -```bash -mkdir -p out/validation -git show 55320bd:analyseur.c > out/validation/analyseur_initial.c -gcc -O0 out/validation/analyseur_initial.c -o out/validation/analyseur_initial -gcc -O0 analyseur.c -o out/validation/analyseur_final -gcc -O0 -pg analyseur.c -o out/validation/analyseur_pg -cd out/validation -/usr/bin/time -f 'real=%e s; RSS_max=%M Kio' ./analyseur_initial -/usr/bin/time -f 'real=%e s; RSS_max=%M Kio' ./analyseur_final -./analyseur_pg -gprof -b ./analyseur_pg gmon.out > profil_apres.txt -``` - -Pour comparer les performances, répéter les deux exécutions sans `-pg` -cinq fois chacune, en alternant leur ordre, puis retenir les médianes. -Chaque exécution doit afficher l'empreinte `1d53de903efd2b7c`. - -* Adrien RABOT -* Théo ANASTASIO \ No newline at end of file +Sources : [notes](notes.md), [profil initial](out/profil_avant.txt), +[profil final](out/profil_apres_tri.txt) et [mesures globales](out/mesures_bilan.csv).