diff --git a/analyseur.c b/analyseur.c index 435d719..ba4c84f 100644 --- a/analyseur.c +++ b/analyseur.c @@ -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); diff --git a/notes.md b/notes.md index ae60154..60a6664 100644 --- a/notes.md +++ b/notes.md @@ -218,4 +218,65 @@ Empreinte du rapport : 1d53de903efd2b7c real 0m1.251s user 0m1.214s sys 0m0.004s -``` \ No newline at end of file +``` + +## 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) +``` diff --git a/out/analyseur b/out/analyseur index b792c3b..5d9206f 100755 Binary files a/out/analyseur and b/out/analyseur differ diff --git a/out/gmon.out b/out/gmon.out index e7a3764..03ef1ab 100644 Binary files a/out/gmon.out and b/out/gmon.out differ diff --git a/out/profil_apres_hachage.txt b/out/profil_apres_hachage.txt new file mode 100644 index 0000000..c2a3287 --- /dev/null +++ b/out/profil_apres_hachage.txt @@ -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 + +[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] +----------------------------------------------- + +[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