Remplacement de la recherche dichotomique par une table de hachage

This commit is contained in:
rabot
2026-10-07 11:51:31 +02:00
parent d3f19871d7
commit 57aaec48d0
5 changed files with 281 additions and 27 deletions
+47 -26
View File
@@ -26,6 +26,8 @@
#define NB_MOTS_VOCAB 7000
#define TAILLE_MOT 32
#define MAX_MOTS_UNIQUES 20000
/* Toujours moins de 50 % de cases occupées, même au maximum de mots. */
#define TAILLE_HACHAGE (2 * MAX_MOTS_UNIQUES + 1)
#define GRAINE 20261006UL
/* ------------------------------------------------------------------------ */
@@ -235,44 +237,54 @@ int est_mot_vide(const char *mot)
return 0;
}
/* Le tableau reste trié alphabétiquement pendant le comptage.
* Renvoie l'indice du mot, ou -(position d'insertion + 1) s'il est absent. */
int chercher_mot(Entree *table, int nb_entrees, const char *mot)
unsigned int hacher_mot(const char *mot)
{
int debut = 0;
int fin = nb_entrees - 1;
while (debut <= fin) {
int milieu = debut + (fin - debut) / 2;
int comparaison = comparer(table[milieu].mot, mot);
if (comparaison == 0)
return milieu;
if (comparaison < 0)
debut = milieu + 1;
else
fin = milieu - 1;
unsigned int h = 2166136261u;
for (int i = 0; mot[i] != '\0'; i++) {
h ^= (unsigned char)mot[i];
h *= 16777619u;
}
return -(debut + 1);
return h;
}
void compter_mot(Entree *table, int *nb_entrees, const char *mot)
/* 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)
{
int indice = chercher_mot(table, *nb_entrees, mot);
int position = hacher_mot(mot) % TAILLE_HACHAGE;
while (index_mots[position] != 0) {
int indice = index_mots[position] - 1;
if (egales(table[indice].mot, mot))
return indice;
/* Collision : essayer la case suivante, avec retour au début. */
position = (position + 1) % TAILLE_HACHAGE;
}
return -(position + 1);
}
void compter_mot(Entree *table, int *nb_entrees, int *index_mots, const char *mot)
{
int indice = chercher_mot(table, index_mots, mot);
if (indice >= 0) {
table[indice].occurrences++;
} else {
indice = -indice - 1;
/* Décaler uniquement lors de la première apparition d'un mot. */
for (int i = *nb_entrees; i > indice; i--)
table[i] = table[i - 1];
if (*nb_entrees >= MAX_MOTS_UNIQUES) {
fprintf(stderr, "Trop de mots distincts (maximum : %d).\n",
MAX_MOTS_UNIQUES);
exit(EXIT_FAILURE);
}
int position = -indice - 1;
indice = *nb_entrees;
copier(table[indice].mot, mot);
table[indice].occurrences = 1;
index_mots[position] = indice + 1;
(*nb_entrees)++;
}
}
void compter_mots_ligne(Entree *table, int *nb_entrees, const char *ligne,
long *nb_mots_total)
void compter_mots_ligne(Entree *table, int *nb_entrees, int *index_mots,
const char *ligne, long *nb_mots_total)
{
char mot[TAILLE_MOT];
int i = 0;
@@ -290,7 +302,7 @@ void compter_mots_ligne(Entree *table, int *nb_entrees, const char *ligne,
if (k > 0) {
(*nb_mots_total)++;
if (!est_mot_vide(mot))
compter_mot(table, nb_entrees, mot);
compter_mot(table, nb_entrees, index_mots, mot);
}
}
}
@@ -428,11 +440,20 @@ int main(void)
normaliser_ligne(lignes[i]);
Entree *table = malloc(MAX_MOTS_UNIQUES * sizeof(Entree));
int *index_mots = calloc(TAILLE_HACHAGE, sizeof(int));
if (table == NULL || index_mots == NULL) {
fprintf(stderr, "Allocation du tableau de comptage impossible.\n");
free(table);
free(index_mots);
exit(EXIT_FAILURE);
}
int nb_entrees = 0;
long nb_mots_total = 0;
for (int i = 0; i < nb_lignes; i++)
compter_mots_ligne(table, &nb_entrees, lignes[i], &nb_mots_total);
compter_mots_ligne(table, &nb_entrees, index_mots, lignes[i], &nb_mots_total);
/* Le tri déplace les entrées : l'index ne sert que pendant le comptage. */
free(index_mots);
trier_entrees(table, nb_entrees);
double moyenne = longueur_moyenne(table, nb_entrees);
+62 -1
View File
@@ -218,4 +218,65 @@ 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)
```
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+172
View File
@@ -0,0 +1,172 @@
Flat profile:
Each sample counts as 0.01 seconds.
% cumulative self self total
time seconds seconds calls ms/call ms/call name
61.64 0.45 0.45 1599863 0.00 0.00 longueur
10.27 0.53 0.07 12172087 0.00 0.00 comparer
9.59 0.59 0.07 1 70.00 195.00 trier_entrees
6.85 0.65 0.05 17202045 0.00 0.00 doit_echanger
5.48 0.69 0.04 250000 0.00 0.00 est_mot_vide
2.05 0.70 0.01 12198772 0.00 0.00 egales
1.37 0.71 0.01 150182 0.00 0.00 chercher_mot
1.37 0.72 0.01 1 10.00 10.00 decouper_lignes
1.37 0.73 0.01 _init
0.00 0.73 0.00 5297256 0.00 0.00 est_lettre
0.00 0.73 0.00 1266663 0.00 0.00 en_minuscule
0.00 0.73 0.00 928117 0.00 0.00 alea
0.00 0.73 0.00 150182 0.00 0.00 compter_mot
0.00 0.73 0.00 150182 0.00 0.00 hacher_mot
0.00 0.73 0.00 32667 0.00 0.00 copier
0.00 0.73 0.00 26801 0.00 0.00 ajouter
0.00 0.73 0.00 21746 0.00 0.00 compter_mots_ligne
0.00 0.73 0.00 21746 0.00 0.02 normaliser_ligne
0.00 0.73 0.00 1 0.00 0.00 analyser_bigrammes
0.00 0.73 0.00 1 0.00 1.65 construire_rapport
0.00 0.73 0.00 1 0.00 1.65 ecart_type_longueur
0.00 0.73 0.00 1 0.00 0.00 empreinte
0.00 0.73 0.00 1 0.00 0.00 generer_texte
0.00 0.73 0.00 1 0.00 5.89 generer_vocabulaire
0.00 0.73 0.00 1 0.00 1.65 longueur_moyenne
0.00 0.73 0.00 1 0.00 0.00 racine
Call graph
granularity: each sample hit covers 4 byte(s) for 1.37% of 0.73 seconds
index % time self children called name
<spontaneous>
[1] 98.6 0.00 0.72 main [1]
0.00 0.44 21746/21746 normaliser_ligne [3]
0.07 0.12 1/1 trier_entrees [4]
0.00 0.07 21746/21746 compter_mots_ligne [7]
0.01 0.00 1/1 decouper_lignes [12]
0.00 0.01 1/1 generer_vocabulaire [15]
0.00 0.00 1/1 construire_rapport [16]
0.00 0.00 1/1 longueur_moyenne [18]
0.00 0.00 1/1 ecart_type_longueur [17]
0.00 0.00 1/1 generer_texte [26]
0.00 0.00 1/1 racine [27]
0.00 0.00 1/1 analyser_bigrammes [24]
0.00 0.00 1/1 empreinte [25]
-----------------------------------------------
0.00 0.00 5866/1599863 longueur_moyenne [18]
0.00 0.00 5866/1599863 ecart_type_longueur [17]
0.01 0.00 26801/1599863 ajouter [14]
0.44 0.00 1561330/1599863 normaliser_ligne [3]
[2] 61.6 0.45 0.00 1599863 longueur [2]
-----------------------------------------------
0.00 0.44 21746/21746 main [1]
[3] 60.2 0.00 0.44 21746 normaliser_ligne [3]
0.44 0.00 1561330/1599863 longueur [2]
0.00 0.00 1539584/5297256 est_lettre [19]
0.00 0.00 1266663/1266663 en_minuscule [20]
-----------------------------------------------
0.07 0.12 1/1 main [1]
[4] 26.7 0.07 0.12 1 trier_entrees [4]
0.05 0.07 17202045/17202045 doit_echanger [5]
-----------------------------------------------
0.05 0.07 17202045/17202045 trier_entrees [4]
[5] 17.1 0.05 0.07 17202045 doit_echanger [5]
0.07 0.00 12172087/12172087 comparer [6]
-----------------------------------------------
0.07 0.00 12172087/12172087 doit_echanger [5]
[6] 10.3 0.07 0.00 12172087 comparer [6]
-----------------------------------------------
0.00 0.07 21746/21746 main [1]
[7] 8.9 0.00 0.07 21746 compter_mots_ligne [7]
0.04 0.01 250000/250000 est_mot_vide [8]
0.00 0.01 150182/150182 compter_mot [11]
-----------------------------------------------
0.04 0.01 250000/250000 compter_mots_ligne [7]
[8] 7.5 0.04 0.01 250000 est_mot_vide [8]
0.01 0.00 12045671/12198772 egales [9]
-----------------------------------------------
0.00 0.00 153101/12198772 chercher_mot [10]
0.01 0.00 12045671/12198772 est_mot_vide [8]
[9] 2.1 0.01 0.00 12198772 egales [9]
-----------------------------------------------
0.01 0.00 150182/150182 compter_mot [11]
[10] 1.4 0.01 0.00 150182 chercher_mot [10]
0.00 0.00 153101/12198772 egales [9]
0.00 0.00 150182/150182 hacher_mot [22]
-----------------------------------------------
0.00 0.01 150182/150182 compter_mots_ligne [7]
[11] 1.4 0.00 0.01 150182 compter_mot [11]
0.01 0.00 150182/150182 chercher_mot [10]
0.00 0.00 5866/32667 copier [23]
-----------------------------------------------
0.01 0.00 1/1 main [1]
[12] 1.4 0.01 0.00 1 decouper_lignes [12]
-----------------------------------------------
<spontaneous>
[13] 1.4 0.01 0.00 _init [13]
-----------------------------------------------
0.00 0.00 5867/26801 construire_rapport [16]
0.00 0.01 20934/26801 generer_vocabulaire [15]
[14] 1.0 0.00 0.01 26801 ajouter [14]
0.01 0.00 26801/1599863 longueur [2]
0.00 0.00 26801/32667 copier [23]
-----------------------------------------------
0.00 0.01 1/1 main [1]
[15] 0.8 0.00 0.01 1 generer_vocabulaire [15]
0.00 0.01 20934/26801 ajouter [14]
0.00 0.00 27934/928117 alea [21]
-----------------------------------------------
0.00 0.00 1/1 main [1]
[16] 0.2 0.00 0.00 1 construire_rapport [16]
0.00 0.00 5867/26801 ajouter [14]
-----------------------------------------------
0.00 0.00 1/1 main [1]
[17] 0.2 0.00 0.00 1 ecart_type_longueur [17]
0.00 0.00 5866/1599863 longueur [2]
-----------------------------------------------
0.00 0.00 1/1 main [1]
[18] 0.2 0.00 0.00 1 longueur_moyenne [18]
0.00 0.00 5866/1599863 longueur [2]
-----------------------------------------------
0.00 0.00 1539584/5297256 normaliser_ligne [3]
0.00 0.00 3757672/5297256 analyser_bigrammes [24]
[19] 0.0 0.00 0.00 5297256 est_lettre [19]
-----------------------------------------------
0.00 0.00 1266663/1266663 normaliser_ligne [3]
[20] 0.0 0.00 0.00 1266663 en_minuscule [20]
-----------------------------------------------
0.00 0.00 27934/928117 generer_vocabulaire [15]
0.00 0.00 900183/928117 generer_texte [26]
[21] 0.0 0.00 0.00 928117 alea [21]
-----------------------------------------------
0.00 0.00 150182/150182 chercher_mot [10]
[22] 0.0 0.00 0.00 150182 hacher_mot [22]
-----------------------------------------------
0.00 0.00 5866/32667 compter_mot [11]
0.00 0.00 26801/32667 ajouter [14]
[23] 0.0 0.00 0.00 32667 copier [23]
-----------------------------------------------
0.00 0.00 1/1 main [1]
[24] 0.0 0.00 0.00 1 analyser_bigrammes [24]
0.00 0.00 3757672/5297256 est_lettre [19]
-----------------------------------------------
0.00 0.00 1/1 main [1]
[25] 0.0 0.00 0.00 1 empreinte [25]
-----------------------------------------------
0.00 0.00 1/1 main [1]
[26] 0.0 0.00 0.00 1 generer_texte [26]
0.00 0.00 900183/928117 alea [21]
-----------------------------------------------
0.00 0.00 1/1 main [1]
[27] 0.0 0.00 0.00 1 racine [27]
-----------------------------------------------
Index by function name
[13] _init [23] copier [26] generer_texte
[14] ajouter [12] decouper_lignes [15] generer_vocabulaire
[21] alea [5] doit_echanger [22] hacher_mot
[24] analyser_bigrammes [17] ecart_type_longueur [2] longueur
[10] chercher_mot [9] egales [18] longueur_moyenne
[6] comparer [25] empreinte [3] normaliser_ligne
[11] compter_mot [20] en_minuscule [27] racine
[7] compter_mots_ligne [19] est_lettre [4] trier_entrees
[16] construire_rapport [8] est_mot_vide