diff --git a/analyseur.c b/analyseur.c index c4e1f23..9194077 100644 --- a/analyseur.c +++ b/analyseur.c @@ -110,6 +110,7 @@ static const char *MOTS_VIDES[] = { "ses", "leur", "leurs", "mon", "ma", "mes", "ne", "pas", "plus", "est" }; #define NB_MOTS_VIDES ((int)(sizeof(MOTS_VIDES) / sizeof(MOTS_VIDES[0]))) +#define TAILLE_HACHAGE_VIDES (2 * NB_MOTS_VIDES + 1) static const char *SYLLABES[] = { "ba", "be", "bi", "bo", "ca", "co", "da", "de", "di", "fa", "fi", "ga", @@ -227,15 +228,6 @@ typedef struct { int taille; } Entree; -int est_mot_vide(const char *mot) -{ - for (int i = 0; i < NB_MOTS_VIDES; i++) { - if (egales(mot, MOTS_VIDES[i])) - return 1; - } - return 0; -} - unsigned int hacher_mot(const char *mot) { unsigned int h = 2166136261u; @@ -246,6 +238,48 @@ unsigned int hacher_mot(const char *mot) return h; } +/* Construire un index séparé sans modifier la liste utilisée par le générateur. + * L'index est initialement rempli de zéros ; chaque case contient indice + 1. */ +static int initialiser_mots_vides(int *index_mots_vides) +{ + int taille_max = 0; + for (int i = 0; i < NB_MOTS_VIDES; i++) { + int taille = 0; + while (MOTS_VIDES[i][taille] != '\0') + taille++; + if (taille > taille_max) + taille_max = taille; + + int position = hacher_mot(MOTS_VIDES[i]) % TAILLE_HACHAGE_VIDES; + while (index_mots_vides[position] != 0 && + !egales(MOTS_VIDES[index_mots_vides[position] - 1], MOTS_VIDES[i])) + position = (position + 1) % TAILLE_HACHAGE_VIDES; + /* Un doublon, comme "ou", réutilise la même case. */ + index_mots_vides[position] = i + 1; + } + return taille_max; +} + +int est_mot_vide(const char *mot, int taille) +{ + static int index_mots_vides[TAILLE_HACHAGE_VIDES]; + static int taille_max = -1; + if (taille_max < 0) + taille_max = initialiser_mots_vides(index_mots_vides); + + /* La taille provient du découpage : aucun nouveau parcours du mot. */ + if (taille > taille_max) + return 0; + + int position = hacher_mot(mot) % TAILLE_HACHAGE_VIDES; + while (index_mots_vides[position] != 0) { + if (egales(mot, MOTS_VIDES[index_mots_vides[position] - 1])) + return 1; + position = (position + 1) % TAILLE_HACHAGE_VIDES; + } + return 0; +} + /* L'index contient indice + 1 ; zéro désigne une case libre. * Renvoie l'indice du mot, ou -(case libre + 1) s'il est absent. */ int chercher_mot(const Entree *table, const int *index_mots, const char *mot) @@ -302,7 +336,7 @@ void compter_mots_ligne(Entree *table, int *nb_entrees, int *index_mots, mot[k] = '\0'; if (k > 0) { (*nb_mots_total)++; - if (!est_mot_vide(mot)) + if (!est_mot_vide(mot, k)) compter_mot(table, nb_entrees, index_mots, mot, k); } } diff --git a/notes.md b/notes.md index 3160449..45e24be 100644 --- a/notes.md +++ b/notes.md @@ -511,3 +511,93 @@ mkdir -p out/apres_bigrammes gcc -O0 -pg analyseur.c -o out/apres_bigrammes/analyseur (cd out/apres_bigrammes && ./analyseur && gprof -b ./analyseur gmon.out > ../profil_apres_bigrammes.txt) ``` + +## Optimisation de `est_mot_vide` + +La recherche linéaire dans les 60 entrées de `MOTS_VIDES` est remplacée +par un index de hachage de 121 cases, construit au premier appel seulement. +Chaque case contient l'indice d'un mot + 1, ou zéro si elle est libre. +Les collisions sont résolues en avançant à la case suivante, avec retour +au début. Le doublon `ou` réutilise la même case : l'index contient donc +59 mots distincts et reste rempli à moins de 50 %. + +L'initialisation calcule aussi la longueur maximale des mots vides, +actuellement 5 caractères. `est_mot_vide` reçoit la longueur `k`, déjà +connue lors du découpage, et rejette immédiatement les mots plus longs. +Sur ce texte, 107 019 mots sont ainsi exclus avant même le hachage. +Les autres recherches sont en O(1) en moyenne pour ces mots de longueur +bornée, avec un pire cas linéaire en cas de nombreuses collisions. + +L'index est séparé de `MOTS_VIDES` : la liste originale, son ordre et son +doublon sont conservés pour produire exactement le même texte. +L'index et la longueur maximale occupent 488 octets sur cette machine, +sans allocation dynamique supplémentaire. + +Profil enregistré dans [out/profil_apres_mots_vides.txt](out/profil_apres_mots_vides.txt). +La comparaison porte sur la version précédente, décrite dans +`out/profil_apres_bigrammes.txt`. + +| Mesure | Avant | Après | +| --- | ---: | ---: | +| Appels à `est_mot_vide` | 250 000 | 250 000 | +| Appels à `egales` depuis `est_mot_vide` | 12 045 671 | 202 444 | +| Appels à `egales` lors de l'initialisation de l'index | 0 | 28 | +| Appels à `egales` dans tout le programme | 12 198 772 | 355 573 | +| Temps réel médian sans profiling (`gcc -O0`, 10 exécutions par version) | 0,1007 s | 0,0543 s | + +La recherche des mots vides effectue environ 60 fois moins de comparaisons. +Le temps total baisse de 46,1 %, soit une accélération d'environ 1,86. +Les mesures ont été réalisées en alternant les deux versions sur la même +machine : + +- Avant : 0,0998 / 0,0953 / 0,0997 / 0,1015 / 0,1170 / 0,1067 / 0,1102 / + 0,0999 / 0,1018 / 0,0989 s. +- Après : 0,0531 / 0,0535 / 0,0565 / 0,0542 / 0,0554 / 0,0544 / 0,0577 / + 0,0579 / 0,0520 / 0,0522 s. + +Validation : compilation sans avertissement avec `-Wall -Wextra -Wpedantic`, +rapport et sortie console identiques octet par octet sur les vingt +exécutions, avec `-pg` et sous AddressSanitizer et UndefinedBehaviorSanitizer. +L'empreinte reste **`1d53de903efd2b7c`**. +Un test temporaire a comparé les réponses à une recherche linéaire pour +les 60 entrées, leurs préfixes et suffixes, les 18 278 chaînes minuscules +de 1 à 3 caractères, 50 000 mots pseudo-aléatoires et les 7 000 mots du +vocabulaire. Un dictionnaire synthétique dans un processus séparé a +également vérifié les collisions avec retour à la première case et +l'adaptation automatique à des mots vides de plus de 5 caractères. + +Commandes pour reproduire ce profil : + +```bash +mkdir -p out/apres_mots_vides +gcc -O0 -pg analyseur.c -o out/apres_mots_vides/analyseur +(cd out/apres_mots_vides && ./analyseur && gprof -b ./analyseur gmon.out > ../profil_apres_mots_vides.txt) +``` + +```bash +[rabot@salle222-12 out]$ gcc -O0 ../analyseur.c -o analyseur +[rabot@salle222-12 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.078s +user 0m0.059s +sys 0m0.001s +``` \ No newline at end of file diff --git a/out/analyseur b/out/analyseur index e030422..45fcfa2 100755 Binary files a/out/analyseur and b/out/analyseur differ