Files
2026_TD1_DEV51_Qualite_Algo/COMPTE_RENDU.md
T

10 KiB
Raw Blame 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

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

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 :

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.