forked from menault/2026_TD1_DEV51_Qualite_Algo
Compte rendu
This commit is contained in:
+289
-1
@@ -1 +1,289 @@
|
||||
// Piste : réduire le temps d'exécution de `doit_echanger`
|
||||
# Compte rendu — Profiling et 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 par version sans
|
||||
instrumentation. Le rapport produit et les statistiques affichées restent
|
||||
identiques. L'empreinte du rapport est toujours **`1d53de903efd2b7c`**.
|
||||
|
||||
## 1. Objectif et méthode de mesure
|
||||
|
||||
Le programme génère un texte pseudo-aléatoire avec une graine fixe
|
||||
(`20261006`), normalise ses lignes, compte les mots hors mots vides, les
|
||||
classe et calcule des statistiques. Le jeu de données contient 250 000
|
||||
mots, répartis sur 21 746 lignes, dont 5 866 mots distincts retenus.
|
||||
|
||||
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.
|
||||
|
||||
Deux compilations sont utilisées :
|
||||
|
||||
- `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`.
|
||||
|
||||
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.
|
||||
|
||||
Les deux premières optimisations ont fait l'objet de mesures ponctuelles.
|
||||
Pour les trois suivantes, les valeurs retenues sont les médianes de cinq
|
||||
exécutions par version, en alternant les versions avant et après modification.
|
||||
|
||||
## 2. Analyse du profil initial
|
||||
|
||||
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.
|
||||
|
||||
| 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 % |
|
||||
|
||||
Le graphe d'appels permet d'expliquer ces résultats :
|
||||
|
||||
- **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.
|
||||
|
||||
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.
|
||||
|
||||
## 3. Pistes d'amélioration, classées par gain attendu
|
||||
|
||||
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.
|
||||
|
||||
| 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 |
|
||||
| --- | ---: | ---: |
|
||||
| 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 |
|
||||
|
||||
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.
|
||||
|
||||
## 6. Validation des résultats
|
||||
|
||||
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`.
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
version;execution;temps_reel_s;temps_user_s;temps_systeme_s;pic_rss_kio
|
||||
initial;1;2.83;2.82;0.00;6664
|
||||
final;1;0.11;0.11;0.00;6672
|
||||
final;2;0.11;0.11;0.00;6664
|
||||
initial;2;2.80;2.80;0.00;6672
|
||||
initial;3;2.81;2.80;0.00;6564
|
||||
final;3;0.11;0.11;0.00;6672
|
||||
final;4;0.11;0.10;0.00;6668
|
||||
initial;4;2.79;2.79;0.00;6672
|
||||
initial;5;2.82;2.81;0.00;6504
|
||||
final;5;0.11;0.11;0.00;6652
|
||||
|
Reference in New Issue
Block a user