Files
2026_TD1_DEV51_Qualite_Algo/notes.md
T

368 lines
14 KiB
Markdown

Lancement du programme après la compilation avec `gcc -O0 analyseur.c -o analyseur`
```bash
Lignes analysées : 21746
Mots au total : 250000
Mots distincts : 5866
Longueur moyenne : 6.712 (écart-type 1.873)
Bigramme le + courant : "me" (17607 fois, souvent suivi de 'n')
Top 10 :
1. rane 287
2. fasi 241
3. tata 219
4. mopo 214
5. cofi 180
6. vesi 179
7. laganomo 177
8. boto 172
9. nopebo 169
10. rotion 165
Empreinte du rapport : 1d53de903efd2b7c
```
Et ensuite la commande time sur l'exécutable avec `time ./analyseur `
```bash
Lignes analysées : 21746
Mots au total : 250000
Mots distincts : 5866
Longueur moyenne : 6.712 (écart-type 1.873)
Bigramme le + courant : "me" (17607 fois, souvent suivi de 'n')
Top 10 :
1. rane 287
2. fasi 241
3. tata 219
4. mopo 214
5. cofi 180
6. vesi 179
7. laganomo 177
8. boto 172
9. nopebo 169
10. rotion 165
Empreinte du rapport : 1d53de903efd2b7c
real 0m2.858s
user 0m2.821s
sys 0m0.013s
```
On recompile avec le flag `-pg` : `gcc -O0 -pg analyseur.c -o analyseur`
Et on relance `time` dessus
```bash
Lignes analysées : 21746
Mots au total : 250000
Mots distincts : 5866
Longueur moyenne : 6.712 (écart-type 1.873)
Bigramme le + courant : "me" (17607 fois, souvent suivi de 'n')
Top 10 :
1. rane 287
2. fasi 241
3. tata 219
4. mopo 214
5. cofi 180
6. vesi 179
7. laganomo 177
8. boto 172
9. nopebo 169
10. rotion 165
Empreinte du rapport : 1d53de903efd2b7c
real 0m6.354s
user 0m6.295s
sys 0m0.020s
```
On lance `gprof` : `gprof -b ./analyseur gmon.out > profil_avant.txt`
## Optimisation de `chercher_mot` : recherche dichotomique
La recherche initiale parcourait toutes les entrées jusqu'à trouver le mot.
Pour 150 182 recherches, elle effectuait 296 656 203 appels à `egales()`.
Le tableau est maintenant maintenu dans l'ordre alphabétique pendant le
comptage. `chercher_mot` utilise `comparer()` pour éliminer la moitié des
entrées restantes à chaque étape : la recherche passe de O(n) à O(log n).
Si le mot est absent, la fonction renvoie `-(position d'insertion + 1)` ;
`compter_mot` retrouve cette position avec `-indice - 1`, décale les entrées
et insère le nouveau mot. Cette insertion reste en O(n), mais elle n'a lieu
qu'à la première apparition de chacun des 5 866 mots distincts.
Le tableau existant est réutilisé, sans allocation supplémentaire.
Le tri final par fréquence produit le même classement.
Résultats du nouveau profil, conservé dans [out/profil_apres.txt](out/profil_apres.txt) :
| Mesure | Avant | Après |
| --- | ---: | ---: |
| Appels de `chercher_mot` | 150 182 | 150 182 |
| Appels à `egales()` depuis `chercher_mot` | 296 656 203 | 0 |
| Comparaisons de chaînes dans `chercher_mot` | 296 656 203 | 1 718 447 |
| Appels à `egales()` dans tout le programme | 308 701 874 | 12 045 671 |
| Temps réel sans profiling (`gcc -O0`) | 2,80 s | 1,45 s |
Les recherches effectuent environ 173 fois moins de comparaisons.
Les appels restants à `egales()` viennent de `est_mot_vide`.
Les temps ci-dessus correspondent à une mesure avant/après sur la même
machine pendant cette modification : environ 48 % de temps en moins
(accélération d'environ 1,93). Ce sont des mesures ponctuelles.
Validation : compilation avec `-Wall -Wextra -Wpedantic`, rapport et sortie
console identiques octet par octet à la version initiale, avec et sans `-pg`.
L'empreinte reste **`1d53de903efd2b7c`**.
Un test temporaire sous AddressSanitizer et UndefinedBehaviorSanitizer a
également vérifié les recherches dans un tableau vide, les mots présents
ou absents, les insertions au début/milieu/fin, les doublons et les mots de
31 caractères, puis comparé 10 000 comptages à une référence linéaire.
Pour reproduire le profil sans écraser les fichiers du profil initial :
```bash
mkdir -p out/apres
gcc -O0 -pg analyseur.c -o out/apres/analyseur
(cd out/apres && ./analyseur && gprof -b ./analyseur gmon.out > ../profil_apres.txt)
```
```bash
Lignes analysées : 21746
Mots au total : 250000
Mots distincts : 5866
Longueur moyenne : 6.712 (écart-type 1.873)
Bigramme le + courant : "me" (17607 fois, souvent suivi de 'n')
Top 10 :
1. rane 287
2. fasi 241
3. tata 219
4. mopo 214
5. cofi 180
6. vesi 179
7. laganomo 177
8. boto 172
9. nopebo 169
10. rotion 165
Empreinte du rapport : 1d53de903efd2b7c
real 0m1.986s
user 0m1.934s
sys 0m0.011s
```
## Optimisation des appels à `longueur_moyenne`
`longueur_moyenne` parcourt les mots une fois par appel. Le coût excessif
venait de `ecart_type_longueur`, qui recalculait cette même moyenne à
chaque itération, soit 5 866 fois. Avec l'appel de `main`, cela représentait
5 867 calculs de moyenne et 34 415 822 appels à `longueur()`.
`main` calcule désormais la moyenne une seule fois et la transmet en
paramètre à `ecart_type_longueur`. La boucle utilise directement cette
valeur. Le calcul des statistiques passe ainsi de O(n²) à O(n), sans
allocation supplémentaire ni modification de la formule de variance.
Profil enregistré dans [out/profil_apres_moyenne.txt](out/profil_apres_moyenne.txt).
La colonne « Avant » correspond à la version déjà optimisée pour
`chercher_mot`, dont le profil reste dans `out/profil_apres.txt`.
| Mesure | Avant | Après |
| --- | ---: | ---: |
| Appels à `longueur_moyenne` | 5 867 | 1 |
| Appels à `longueur()` depuis `longueur_moyenne` | 34 415 822 | 5 866 |
| Appels à `longueur()` dans tout le programme | 36 009 819 | 1 599 863 |
| Temps réel sans profiling (`gcc -O0`) | 1,46 s | 0,88 s |
Cette mesure ponctuelle sur la même machine donne environ 40 % de temps
en moins, soit une accélération d'environ 1,66 par rapport à l'étape précédente.
Validation : compilation avec `-Wall -Wextra -Wpedantic` sans avertissement,
puis comparaison du rapport et de la sortie console avant/après, avec et
sans `-pg`. Ils restent identiques octet par octet, y compris la moyenne
(`6.712`) et l'écart-type (`1.873`). L'empreinte reste **`1d53de903efd2b7c`**.
Commandes pour reproduire ce profil :
```bash
mkdir -p out/apres_moyenne
gcc -O0 -pg analyseur.c -o out/apres_moyenne/analyseur
(cd out/apres_moyenne && ./analyseur && gprof -b ./analyseur gmon.out > ../profil_apres_moyenne.txt)
```
```bash
[rabot@salle235-06 out]$ time ./analyseur
Lignes analysées : 21746
Mots au total : 250000
Mots distincts : 5866
Longueur moyenne : 6.712 (écart-type 1.873)
Bigramme le + courant : "me" (17607 fois, souvent suivi de 'n')
Top 10 :
1. rane 287
2. fasi 241
3. tata 219
4. mopo 214
5. cofi 180
6. vesi 179
7. laganomo 177
8. boto 172
9. nopebo 169
10. rotion 165
Empreinte du rapport : 1d53de903efd2b7c
real 0m1.251s
user 0m1.214s
sys 0m0.004s
```
## Remplacement de la dichotomie par une table de hachage
Un index de 40 001 cases associe chaque mot à son entrée dans le tableau.
`hacher_mot` calcule un entier à partir des caractères du mot ; le modulo
donne la première case à examiner. Chaque case contient l'indice de
l'entrée + 1, ou zéro si elle est libre.
En cas de collision, `chercher_mot` vérifie le mot avec `egales()` puis
avance à la case suivante, en revenant au début si nécessaire. Deux mots
ayant le même hachage restent donc bien distincts. Les nouveaux mots sont
ajoutés à la fin du tableau, sans décaler les entrées existantes.
La recherche et l'insertion sont en O(1) en moyenne pour ces mots de
longueur bornée, avec un pire cas en O(n) en cas de nombreuses collisions.
La limite de 20 000 mots distincts est vérifiée avant chaque nouvelle
insertion ; avec 40 001 cases, l'index reste rempli à moins de 50 %.
Ce choix ajoute `40 001 * sizeof(int)` octets, soit environ 156 Kio sur
cette machine. L'index est libéré après le comptage, avant le tri final
qui déplace les entrées. L'empreinte du rapport est conservée, mais cette
version utilise davantage de mémoire pendant le comptage.
Profil enregistré dans [out/profil_apres_hachage.txt](out/profil_apres_hachage.txt).
La comparaison porte sur la version précédente avec dichotomie et moyenne
calculée une seule fois, conservée dans `out/profil_apres_moyenne.txt`.
| Mesure | Dichotomie | Hachage |
| --- | ---: | ---: |
| Appels de `chercher_mot` | 150 182 | 150 182 |
| Comparaisons de chaînes dans `chercher_mot` | 1 718 447 (`comparer`) | 153 101 (`egales`) |
| Temps réel médian sans profiling (`gcc -O0`, 5 exécutions par version) | 0,888 s | 0,831 s |
Les recherches font environ 11,2 fois moins de comparaisons. Le temps
total baisse d'environ 6,4 % : le gain reste limité car la normalisation
et le tri occupent désormais l'essentiel du temps. L'ordre d'arrivée des
mots remplace l'ordre alphabétique pendant le comptage ; le tri final
effectue donc un nombre différent de comparaisons, tout en produisant
le même classement.
Les cinq mesures, réalisées en alternant les deux versions sur la même
machine, sont respectivement : 0,883 / 0,892 / 0,888 / 0,883 / 0,888 s
avant, et 0,831 / 0,830 / 0,831 / 0,832 / 0,829 s après.
Validation : compilation sans avertissement avec `-Wall -Wextra -Wpedantic`,
rapport et sortie console identiques octet par octet sur les dix
exécutions et avec `-pg`. L'empreinte reste **`1d53de903efd2b7c`**.
Des tests temporaires sous AddressSanitizer et UndefinedBehaviorSanitizer
ont vérifié les collisions avec retour à la première case, les doublons,
les mots de 0/1/31 caractères, 10 000 comptages comparés à une référence
linéaire, les 20 000 entrées possibles et le refus d'un dépassement.
Le programme complet a également été exécuté avec ces deux outils,
sans erreur et avec une sortie identique.
Commandes pour reproduire ce profil :
```bash
mkdir -p out/apres_hachage
gcc -O0 -pg analyseur.c -o out/apres_hachage/analyseur
(cd out/apres_hachage && ./analyseur && gprof -b ./analyseur gmon.out > ../profil_apres_hachage.txt)
```
## Longueurs mémorisées et suppression des parcours répétés
Le champ `taille` de `Entree` mémorise la longueur du mot à son insertion.
La valeur vient directement de `k`, déjà calculé dans `compter_mots_ligne` :
aucun parcours supplémentaire n'est nécessaire. La moyenne et la variance
lisent désormais ce champ. Le tri déplace l'entrée entière, donc la longueur
reste associée au bon mot. Pour un mot tronqué, la taille enregistrée est
celle du mot conservé, soit au maximum 31 caractères.
Deux autres changements éliminent les recalculs sur les chaînes longues :
- `normaliser_ligne` s'arrête directement sur `ligne[i] == '\0'`. Le parcours
d'une ligne de L caractères passe de O(L²) à O(L).
- `ajouter` reçoit une position de fin, qu'il avance pendant la copie des
caractères ajoutés. Cette position est conservée lors de la construction
du rapport et réinitialisée pour chaque mot du vocabulaire. Le préfixe
déjà construit n'est plus parcouru à chaque ajout.
La fonction `longueur`, devenue inutilisée, a été supprimée.
Le champ supplémentaire fait passer `sizeof(Entree)` de 36 à 40 octets sur
cette machine : le tableau alloué pour 20 000 entrées prend 80 000 octets
supplémentaires, soit environ 78 Kio.
Profil enregistré dans [out/profil_apres_longueurs.txt](out/profil_apres_longueurs.txt).
La version précédente, avec la table de hachage, est décrite dans
`out/profil_apres_hachage.txt`.
| Mesure | Avant | Après |
| --- | ---: | ---: |
| Appels à `longueur()` depuis les statistiques | 11 732 | 0 |
| Appels à `longueur()` depuis `normaliser_ligne` | 1 561 330 | 0 |
| Appels à `longueur()` depuis `ajouter` | 26 801 | 0 |
| Appels à `longueur()` au total | 1 599 863 | 0 |
| Temps réel médian sans profiling (`gcc -O0`, 5 exécutions par version) | 0,832 s | 0,362 s |
Le temps total baisse de 56,5 %, soit une accélération d'environ 2,30.
Cette mesure porte sur l'ensemble des trois changements, et ne doit pas
être attribuée uniquement au stockage de la longueur des mots.
Les cinq mesures, réalisées en alternant les deux versions sur la même
machine, sont respectivement : 0,835 / 0,838 / 0,830 / 0,832 / 0,829 s
avant, et 0,362 / 0,369 / 0,366 / 0,362 / 0,361 s après.
Validation : compilation sans avertissement avec `-Wall -Wextra -Wpedantic`,
rapport et sortie console identiques octet par octet sur les dix exécutions,
avec `-pg` et sous AddressSanitizer et UndefinedBehaviorSanitizer.
L'empreinte reste **`1d53de903efd2b7c`**, la moyenne `6.712` et l'écart-type
`1.873`. Des tests temporaires ont également vérifié les ajouts vides et
successifs jusqu'à la limite d'un tampon, la normalisation, les longueurs
avant/après tri, les statistiques, le rapport vide et les mots tronqués.
Commandes pour reproduire ce profil :
```bash
mkdir -p out/apres_longueurs
gcc -O0 -pg analyseur.c -o out/apres_longueurs/analyseur
(cd out/apres_longueurs && ./analyseur && gprof -b ./analyseur gmon.out > ../profil_apres_longueurs.txt)
```
```bash
[rabot@salle235-06 out]$ time ./analyseur
Lignes analysées : 21746
Mots au total : 250000
Mots distincts : 5866
Longueur moyenne : 6.712 (écart-type 1.873)
Bigramme le + courant : "me" (17607 fois, souvent suivi de 'n')
Top 10 :
1. rane 287
2. fasi 241
3. tata 219
4. mopo 214
5. cofi 180
6. vesi 179
7. laganomo 177
8. boto 172
9. nopebo 169
10. rotion 165
Empreinte du rapport : 1d53de903efd2b7c
real 0m0.751s
user 0m0.703s
sys 0m0.004s
```