forked from menault/2026_TD1_DEV51_Qualite_Algo
204 lines
10 KiB
Markdown
204 lines
10 KiB
Markdown
|
|
# 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.
|