forked from menault/2026_TD1_DEV51_Qualite_Algo
Documenter le TD et conserver les profils et mesures reproductibles
This commit is contained in:
+203
@@ -0,0 +1,203 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user