Files
2026_TD1_DEV51_Qualite_Algo/COMPTE_RENDU.md
T

204 lines
10 KiB
Markdown
Raw Normal View History

# TD R5.A.04 — Profiling avec gprof
## Objectif et environnement
Analyser l'analyseur de fréquence de mots, identifier les coûts, optimiser une
chose à la fois et conserver exactement les résultats. Travail réalisé le
7 octobre 2026 dans le fork local existant `couchot/2026_TD1_DEV51_Qualite_Algo`.
Les commits sont locaux ; aucun envoi au serveur n'a été effectué.
- Linux x86_64, Intel Core i7-8700 à 3,20 GHz, 12 processeurs logiques.
- GCC 16.2.1, outils GNU `time` et `gprof`.
- Même scénario : 250 000 mots, vocabulaire de 7 000 mots, graine `20261006UL`.
- Toutes les comparaisons utilisent `-O0 -g`, conformément au TD.
- Trois exécutions sans `-pg` par version ; médiane du temps réel retenue.
- Le pic mémoire est le maximum de mémoire résidente indiqué par GNU `time`
(`%M`, en Kio), et non une mesure des allocations totales.
La source initiale est conservée dans `mesures/analyseur_avant.c`. Les sorties,
mesures brutes et le rapport initial sont dans `mesures/`.
## 1. Mesurer avant d'optimiser
```sh
gcc -O0 -g -Wall -Wextra analyseur.c -o analyseur
/usr/bin/time -f 'reel=%e utilisateur=%U systeme=%S memoire_Kio=%M' ./analyseur
```
Avant modification : temps réels **3,14 / 3,01 / 2,91 s**, médiane **3,01 s** ;
temps utilisateur **3,09 / 2,99 / 2,89 s**, médiane **2,99 s**.
Pic mémoire médian : **6 668 Kio**.
Résultats de référence :
- 21 746 lignes, 250 000 mots, 5 866 mots distincts.
- Longueur moyenne : 6,712 ; écart-type : 1,873.
- Bigramme `me` : 17 607 occurrences, souvent suivi de `n`.
- Premier mot : `rane`, 287 occurrences.
- Empreinte : **`1d53de903efd2b7c`**.
## 2. Profiler
```sh
gcc -O0 -g -pg analyseur.c -o analyseur
/usr/bin/time ./analyseur
mv gmon.out gmon_avant.out
gprof -b ./analyseur gmon_avant.out > profil_avant.txt
```
Les binaires et `gmon` avant/après ont été conservés séparément pendant le travail
pour ne pas analyser un ancien profil avec un nouveau binaire. Les profils texte
sont versionnés ; les binaires et fichiers `gmon` sont reproductibles et ignorés.
Avec instrumentation, avant : **6,64 s réelles**, **6,58 s utilisateur**.
Le profil plat cumule **2,71 s échantillonnées**. Ces valeurs ne sont pas
interchangeables : instrumentation, échantillonnage, appels de bibliothèque
non instrumentés et attentes influencent les mesures. Le gain global est donc
calculé sur les exécutions sans instrumentation.
## 3. Analyser le profil plat et le graphe d'appels
| Fonction | Temps propre | Part du profil | Appels |
|---|---:|---:|---:|
| `longueur` | 0,92 s | 33,76 % | 36 009 819 |
| `egales` | 0,89 s | 32,84 % | 308 701 874 |
| `chercher_mot` | 0,37 s | 13,65 % | 150 182 |
| `comparer` | 0,12 s | 4,61 % | 12 172 087 |
| `longueur_moyenne` | 0,11 s | 4,06 % | 5 867 |
| `doit_echanger` | 0,05 s | 1,85 % | 17 202 045 |
Le graphe explique les appels :
1. `main → compter_mots_ligne → compter_mot → chercher_mot → egales`.
`chercher_mot` provoque **296 656 203** comparaisons, car il parcourt les
entrées déjà connues pour chaque mot retenu. Coût O(M × U), avec M le nombre
de mots non vides et U le nombre de mots distincts.
2. `main → ecart_type_longueur → longueur_moyenne → longueur`.
La moyenne est recalculée **5 866 fois** dans la boucle de variance :
**34 415 822** appels à `longueur` viennent du calcul de moyenne.
Les statistiques sont inutilement O(U²).
3. `main → trier_entrees → doit_echanger → comparer`.
Le tri à bulles effectue U(U−1)/2 = **17 202 045** comparaisons, même sans
nécessité d'échanger : O(U²).
4. `normaliser_ligne → longueur` : **1 561 330** appels.
La longueur est parcourue à chaque condition de boucle : O(L²) par ligne.
5. `construire_rapport → ajouter → longueur` : **5 867** appels à `ajouter`.
Chaque ajout reparcourt tout le rapport déjà construit. Pour une longueur
moyenne de ligne bornée, le coût devient O(U²).
Le graphe attribue environ 45,2 % du temps cumulé à `chercher_mot`, 36,3 % à la
variance et 7,6 % au tri. Ce sont des estimations : gprof répartit le temps d'un
appelé entre ses parents en proportion du nombre d'appels. Un appel à `longueur`
sur un rapport de milliers de caractères coûte bien davantage qu'un appel sur
un mot ; cette répartition peut donc sous-estimer la construction du rapport.
Un temps affiché à 0,00 s ne signifie pas un coût nul.
## 4. Pistes proposées, classées par gain attendu
1. Indexer les mots par hachage : supprimer les centaines de millions de
comparaisons linéaires. Gain attendu très élevé.
2. Calculer la moyenne une seule fois avant la boucle de variance : supprimer
un parcours complet par mot distinct. Gain attendu élevé.
3. Remplacer le tri à bulles par un tri en O(U log U). Gain attendu important.
4. Normaliser les lignes en un seul parcours. Gain attendu plus modéré.
5. Conserver la position d'écriture dans le rapport. Gain potentiellement
important malgré son faible poids apparent dans le graphe d'appels.
6. Ensuite, indexer les mots vides ou filtrer par longueur avant comparaison.
Piste laissée pour une seconde itération : le profil final permet d'en
évaluer la priorité. Compiler en `-O2` serait aussi utile pour un produit
livré, mais ferait varier un autre facteur que les algorithmes dans ce TD.
## 5. Optimisations réalisées et mesures intermédiaires
Chaque ligne ajoute une seule optimisation à la version précédente. Chaque
version a été compilée avec `-Wall -Wextra` sans avertissement et exécutée trois
fois. Le rapport et la sortie standard ont été comparés octet par octet à la
référence après chaque exécution.
| Version cumulative | Commit | Temps réels (s) | Médiane (s) | Gain sur l'étape précédente |
|---|---|---|---:|---:|
| Initiale | `6165b37` | 3,14 / 3,01 / 2,91 | 3,01 | — |
| Index de hachage | `2db740d` | 1,50 / 1,48 / 1,47 | 1,48 | 50,8 % |
| Moyenne hors boucle | `5d71287` | 0,88 / 0,90 / 0,86 | 0,88 | 40,5 % |
| Tri fusion | `dddcfd4` | 0,67 / 0,62 / 0,61 | 0,62 | 29,5 % |
| Normalisation linéaire | `6ab37d6` | 0,47 / 0,46 / 0,47 | 0,47 | 24,2 % |
| Position d'écriture du rapport | `1d07578` | 0,18 / 0,14 / 0,20 | 0,18 | 61,7 % |
Détails et compromis :
- **Hachage** : 65 536 cases, indices vers les entrées et sondage linéaire
pour résoudre les collisions. Une égalité de hachage ne suffit jamais : les
chaînes sont comparées. L'ordre d'insertion des entrées est conservé.
Charge de 9 % sur ce scénario, au plus 31 % pour 20 000 entrées ; environ
**256 Kio** supplémentaires. Recherche en O(1) en moyenne, O(U) au pire.
L'index sert au comptage et n'est plus consulté après le tri.
- **Variance** : la moyenne ne dépend pas de l'indice ; la calculer avant la
boucle ramène le calcul à O(U), en conservant l'ordre de sommation flottante.
- **Tri fusion** : même ordre, occurrences décroissantes puis mots croissants.
O(U log U) garanti, tampon temporaire O(U), soit **211 176 octets** ici.
- **Normalisation** : arrêt sur le caractère nul, O(L) sans parcours répétés.
- **Rapport** : écriture directe à l'offset courant avec `snprintf` et contrôle
de la capacité. Travail proportionnel à la taille produite, sans rescan du
préfixe et sans buffer de ligne intermédiaire.
## 6. Re-profiler et valider
Le profil final est dans `profil_apres.txt` :
| Indicateur gprof | Avant | Après |
|---|---:|---:|
| Appels à `egales` | 308 701 874 | 12 195 414 |
| Appels à `longueur` | 36 009 819 | 38 532 |
| Appels à `longueur_moyenne` | 5 867 | 2 |
| Appels à `doit_echanger` | 17 202 045 | 66 114 |
| Appels à `comparer` | 12 172 087 | 33 591 |
| Temps propre total échantillonné | 2,71 s | 0,14 s |
| Temps réel instrumenté | 6,64 s | 0,34 s |
Après optimisation, `egales` représente 60,71 % du profil, essentiellement
pour la recherche des mots vides : celle-ci conserve **12 045 671** appels.
L'index des mots ne provoque plus que **149 743** appels à `egales`.
`analyser_bigrammes` représente 14,29 %. Sur seulement environ 14 échantillons,
ces pourcentages sont peu précis ; les nombres d'appels restent plus utiles.
La récursion du tri fusion est présentée de manière spécifique par gprof :
le chiffre 1 du profil plat n'est pas le nombre total d'invocations récursives.
Sans instrumentation : **3,01 / 0,18 = 16,72**, soit un programme environ
**16,7 fois plus rapide** et une réduction du temps réel de **94,0 %**.
La médiane du temps utilisateur passe de **2,99 à 0,12 s** (réduction de 96,0 %).
Le pic mémoire médian passe de **6 668 à 6 980 Kio** (+312 Kio, soit +4,7 %).
Les pics mesurés reflètent les allocations touchées et l'allocateur ; ils ne
sont pas la somme des tailles théoriques des structures.
L'empreinte reste **`1d53de903efd2b7c`** à toutes les étapes. La comparaison
binaire confirme aussi l'identité du rapport complet et de toute la sortie,
notamment des statistiques que l'empreinte du rapport ne couvre pas.
AddressSanitizer et UndefinedBehaviorSanitizer n'ont signalé aucune erreur sur
le scénario du TD. LeakSanitizer ne fonctionne pas sous le traçage de cet
environnement ; la vérification a donc été relancée avec
`ASAN_OPTIONS=detect_leaks=0`. L'absence de fuite n'est pas certifiée par ce test.
## Reproduire
Depuis la racine du dépôt, avec GCC, GNU time, gprof et Python 3 :
```sh
python3 reproduire_mesures.py
```
Le script compile les sources avant/après, mesure trois exécutions de chacune,
compare rapports et sorties, puis produit les profils dans un nouveau dossier
`mesures_reproduction/`. Il refuse d'écraser un dossier existant ; utiliser
`--destination /tmp/autre_mesure` pour une nouvelle campagne. Il a également
été exécuté intégralement pour vérifier son fonctionnement : cette seconde
campagne donne des médianes de **2,88 s avant et 0,11 s après** (environ ×26,2).
Les fichiers texte correspondants sont conservés dans `mesures/verification/`.
La variation du gain confirme la sensibilité des mesures courtes ; le bilan
principal utilise les mesures de la première campagne, sans mélanger les deux.
Les chiffres dépendent de cette machine, de sa charge et de ce scénario.
Les versions finales étant courtes, les variations relatives sont plus fortes
(0,14 à 0,20 s). Une campagne plus longue et un scénario représentatif seraient
nécessaires pour généraliser ces gains à d'autres usages.