Optimise la fonction est_mot_vide en remplaçant la recherche linéaire par un index de hachage, réduisant ainsi le temps de recherche et améliorant les performances globales.

This commit is contained in:
rabot
2026-10-07 13:59:32 +02:00
parent e3076e46aa
commit 13318119ae
3 changed files with 134 additions and 10 deletions
+44 -10
View File
@@ -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);
}
}
+90
View File
@@ -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
```
BIN
View File
Binary file not shown.