Twitter iPhone pliant OnePlus 11 PS5 Disney+ Orange Livebox Windows 11

Valeur vs représentation d'un objet

62 réponses
Avatar
candide
Bonjour,

Je suis en train d'examiner le code source de différentes tables de
hachage _génériques_ [ie pouvant hacher n'importe quel type de données
(entiers, structures, etc) et pas seulement des chaînes de caractères].

Je suis surpris que les fonctions de hachage de la clé (qui se présenter
typiquement en void *key) se basent sur la seule représentation de
l'objet à hacher plutôt que sur sa valeur.

Voici un cas typique, cf.
https://github.com/cgjones/android-system-core/blob/master/libcutils/hashmap.c
:


int hashmapHash(void* key, size_t keySize) {
int h = keySize;
char* data = (char*) key;
size_t i;
for (i = 0; i < keySize; i++) {
h = h * 31 + *data;
data++;
}
return h;
}

Si la fonction de hachage ci-dessus est appliquée à une chaîne de
caractères, il me semble qu'elle ne pose aucun problème. En revanche si
la clé est d'un autre type comme int ou même unsigned int, il pourrait
en aller autrement. En effet, la norme du langage C n'assure pas du tout
que deux objets ayant même valeur aient nécessairement même
représentation. Si, par exemple, l'entier disons 421, est susceptible
d'avoir deux représentations différentes (autrement dit memcmp ne
renverrait pas 0) deux clés de même valeur, par exemple, 420+1 et 422-1,
pourraient renvoyer des hash différents : catastrophe.


Merci pour vos réponses.

10 réponses

1 2 3 4 5
Avatar
Jean-Marc Bourguet
candide writes:

Donc, pour résumer, si je fais une table de hachage "relativement"
générique en C, j'écris des fonctions de hachage pour les types de base qui
m'intéressent, disons les entiers et les strings ; pour des conteneurs
comme des tableaux ou des structures, j'écris des fonctions de hachage ad
hoc qui vont dépendre des fonctions de hachage pour les types élémentaires
entier ou string présents dans le conteneur. Et pour écrire une fonction de
hachage pour des entiers, je ne le découpe pas en octets mais j'utilise la
valeur de l'entier. C'est à peu près ça ?



Oui.

--
Jean-Marc
FAQ de fclc: http://www.levenez.com/lang/c/faq
Site de usenet-fr: http://www.usenet-fr.news.eu.org
Avatar
Benoit Izac
Dans le message , le 21/08/2013 à 19:44, j'ai
écrit :

Oui, c'est exactement mon souci. Ce qui me gêne, c'est cette note de bas
de page de la Norme :

-------------------------------------------------------------------------
It is possible for objects x and y with the same effective type T to
have the same value when they are accessed as objects of type T, but to
have different values in other contexts. In particular, if == is defined
for type T, then x == y does not imply that memcmp(&x, &y, sizeof (T)) == 0.
-------------------------------------------------------------------------



Oui. Le cas doit être extrèmement rare avec des entiers, plus commun
avec les flottants.

Mais ce sont surtout les structures qui sont le cas courant, puisqu'on
peut faire x= y et avoir ensuite memcmp(&x, &y, sizeof (T)) != 0.



Il y a un quelque chose qui m'échappe : memcmp() compare après avoir
converti en unsigned char, comment est-ce possible que ça diffère sur des
int ou float (pour une structure, je comprends bien qu'il peut y avoir
du padding) ?



Pour les flottants, je veux dire en dehors de 0 qui peut effectivement être
différent en fonction du bit pour le signe.

--
Benoit Izac
Avatar
Samuel DEVULDER
Le 18/08/2013 12:05, candide a écrit :

int hashmapHash(void* key, size_t keySize) {
int h = keySize;
char* data = (char*) key;
size_t i;
for (i = 0; i < keySize; i++) {
h = h * 31 + *data;
data++;
}
return h;
}



Les tables de hachages ont besoin de deux notions:
1/ int hashCode(element)
2/ Une fonction de test d'égalité: boolean equals(element1, element2)

Avec une contrainte forte:
(i) equals(x, y) ==> (hashCode(x) == hashCode(y))

Dans l'implémentation que tu indique il manque, à mon sens, la fonction
de test d'égalité de deux éléments pour être vraiment complète.

L'égalité bit à bit des éléments pose un problème en C bien plus évident
que l'histoire du 120+1 ou 122-1. Deux structures sont elles identiques
même si les octets de bourrages sont différents? Si l'on impose OUI à
cette question, alors la fonction de hashCode ci dessous est fausse car
elle ne vérifie pas le contrat dicté par (i).

Si l'on veut faire un truc générique 100% fiable, il faut considérer que
equals() représente l'identité des éléments (mêmes adresses mémoire),
auquel cas le hashCode() n'est plus ou moins qu'un cast de l'adresse en
entier. C'est évidemment moins puissant que l'implémentation proposé,
mais ce sera probablement plus fiable. D'ailleurs en java, la plupart
des VM implémentent Object.hashCode() ainsi.

Après si l'on veut spécialiser hashCode(element) pour certain type
d'éléments, le C montre ses limites car on a besoin du polymorphisme.

On peut éventuellement s'en sortir en passant des pointeurs sur les
fonctions hashCode() et equals() pour le type considéré. Cependant si
celui ci est inconnu (parce que l'on a une table de hachage dont les
clefs peuvent être de différent type par exemple), il faudrait ruser
bien plus et probablement passer par une structure commune contenant des
pointeurs sur fonctions fournissant les services nécessaires:

enum TYPE {type1, type2, ...};

struct hash_helper {
enum TYPE (*type)(hash_helper *); // type de l'objet pointé
void * (*start)(hash_helper *); // retourne le pointeur sur l'objet
à partir du pointeur de sa sous-structure hash_helper
int (*hashCode)(hash_helper *); // hashCode de l'objet passé en
argument
boolean (*equals)(hash_helper *, hash_helper *); // equalité
de deux objets
}

(C'est, je crois, un truc qui se faisait dans les années 90.. peut-être
du coté des fonctions générique de la libjpeg. cf les JMETHOD de
http://mamedev.org/source/src/lib/libjpeg/jpeglib.h.html )

En bref, cela revient à émuler le polymorphisme en C. C'est probablement
une très mauvaise idée (verbeux, peu robuste, difficile à maintenir). Il
vaut mieux dans ce cas utiliser des langages intégrant le polymorphisme
nativement.

sam.
Avatar
Samuel DEVULDER
Le 18/08/2013 12:05, candide a écrit :

int hashmapHash(void* key, size_t keySize) {
int h = keySize;
char* data = (char*) key;
size_t i;
for (i = 0; i < keySize; i++) {
h = h * 31 + *data;
data++;
}
return h;
}



Les tables de hachages ont besoin de deux notions:
1/ int hashCode(element)
2/ Une fonction de test d'égalité: boolean equals(element1, element2)

Avec une contrainte forte:
(i) equals(x, y) ==> (hashCode(x) == hashCode(y))

Dans l'implémentation que tu indique il manque, à mon sens, la fonction
de test d'égalité de deux éléments pour être vraiment complète.

L'égalité bit à bit des éléments pose un problème en C bien plus évident
que l'histoire du 120+1 ou 122-1. Deux structures sont elles identiques
même si les octets de bourrages sont différents? Si l'on impose OUI à
cette question, alors la fonction de hashCode ci dessus est fausse car
elle ne vérifie pas le contrat dicté par (i).

Si l'on veut faire un truc générique 100% fiable, il faut considérer que
equals() représente l'identité des éléments (mêmes adresses mémoire),
auquel cas le hashCode() n'est plus ou moins qu'un cast de l'adresse en
entier. C'est évidemment moins puissant que l'implémentation proposé,
mais ce sera probablement plus fiable. D'ailleurs en java, la plupart
des VM implémentent Object.hashCode() ainsi.

Après si l'on veut spécialiser hashCode(element) pour certain type
d'éléments, le C montre ses limites car on a besoin du polymorphisme.

On peut éventuellement s'en sortir en passant des pointeurs sur les
fonctions hashCode() et equals() pour le type considéré. Cependant si
celui ci est inconnu (parce que l'on a une table de hachage dont les
clefs peuvent être de différent type par exemple), il faudrait ruser
bien plus et probablement passer par une structure commune contenant des
pointeurs sur fonctions fournissant les services nécessaires:

enum TYPE {type1, type2, ...};

struct hash_helper {
enum TYPE (*type)(hash_helper *); // type de l'objet pointé
void * (*start)(hash_helper *); // retourne le pointeur sur
l'objet à partir du pointeur de sa sous-structure hash_helper
int (*hashCode)(hash_helper *); // hashCode de l'objet passé
en argument
boolean (*equals)(hash_helper *, hash_helper *); // equalité de
deux objets
}

(C'est, je crois, un truc qui se faisait dans les années 90.. peut-être
du coté des fonctions générique de la libjpeg. cf les JMETHOD de
http://mamedev.org/source/src/lib/libjpeg/jpeglib.h.html )

En bref, cela revient à émuler le polymorphisme en C. C'est probablement
une très mauvaise idée (verbeux, peu robuste, difficile à maintenir). Il
vaut mieux dans ce cas utiliser des langages intégrant le polymorphisme
nativement.

sam.
Avatar
Marc Boyer
Le 21-08-2013, candide a écrit :
Le 21/08/2013 09:50, Marc Boyer a écrit :

Ceci dit, dans le code que tu présentes, la fonction hashmapCreate
prend en paramètre une fonction de hash et une fonction d'égalité. C'est
à l'utilisateur de passer en paramètre les fonctions adéquates.



Certes mais je ne vois pas en quoi ça change le problème.



Ca change le problème dans le sens où cette interface dit explictement
à l'utilisateur que c'est à lui de donner les fonctions de hash et
de comparaison des clés.



Ah oui tu as raison, je n'avais pas réalisé que la fonction de hachage
générique hashmapHash n'est jamais utilisée dans le code (mais alors
pourquoi l'ont-il placé là ?)



Je ne connais pas les intentions des développeurs. Mais fournir
une fonction de hash qui marche pour un type "simple" simplifie
la vie de l'utilisateur qui peut ré-utiliser cette fonction.

Je suppose que j'ai deux structures X et Y. Je peux faire mes
fonctions de hash construites à partir de la fonction fournie.

typedef struct {
char c;
int i;
} X;

typedef struct {
int i;
char * string; // nul-terminated
} Y;

int hashmapHash(void* key, size_t keySize);

int hashX(X* x){
return hashmapHash( &(x->c) , 1 ) + hashmapHash( &(x->i), sizeof( x->i ) );
}

int hashY(Y* y){
return hashmapHash( &(x->i), sizeof( x->i ) )
+ hashmapHash( x->string, strlen( x->string) );
}

Marc Boyer
--
À mesure que les inégalités regressent, les attentes se renforcent.
François Dubet
Avatar
Marc Boyer
Le 21-08-2013, Benoit Izac a écrit :
Bonjour,

le 21/08/2013 à 09:50, Marc Boyer a écrit dans le message
<kv1rgd$kjm$ :

Oui, c'est exactement mon souci. Ce qui me gêne, c'est cette note de bas
de page de la Norme :

-------------------------------------------------------------------------
It is possible for objects x and y with the same effective type T to
have the same value when they are accessed as objects of type T, but to
have different values in other contexts. In particular, if == is defined
for type T, then x == y does not imply that memcmp(&x, &y, sizeof (T)) == 0.
-------------------------------------------------------------------------



Oui. Le cas doit être extrèmement rare avec des entiers, plus commun
avec les flottants.

Mais ce sont surtout les structures qui sont le cas courant, puisqu'on
peut faire x= y et avoir ensuite memcmp(&x, &y, sizeof (T)) != 0.



Il y a un quelque chose qui m'échappe : memcmp() compare après avoir
converti en unsigned char, comment est-ce possible que ça diffère sur des
int ou float (pour une structure, je comprends bien qu'il peut y avoir
du padding) ?



Première hypothèse (théorique): j'ai une machine qui ne sait adresser
que des mots de 8 ou 32 bits. Je décide d'encoder des shorts sur 16 bits.
Je décide alors de faire sizeof(short)=4 (32 bits) et de n'en utiliser
que 16, et je laisse les 16 autre dans un état "indéterminé".

Pour les flottants, je suis incompétent: je sais qu'on peut avoir +0.0
et -0.0 et NaN dans IEEE-754, mais la norme C admet surement d'autres
encodages.

Au final, je me méfierais avant d'écrire une table de hash sur
des flottants comme clef.


Marc Boyer
--
À mesure que les inégalités regressent, les attentes se renforcent.
François Dubet
Avatar
Marc Boyer
Le 21-08-2013, candide a écrit :
Le 21/08/2013 15:00, Marc Boyer a écrit :

Dans celle là, j'ai bien trouvé une fonction de hash sur les chaines
de caractères, mais c'est tout.




Moi je parle de htab_hash_string (const PTR p) ? Le commentaire dit :


Hash P _as_ a null-terminated string

(soulignement de moi) et PTR est une macro pour void *, donc il s'agit
d'une fonction de hachage générique. Idem pour iterative_hash. Mais
effectivement ces fonctions ne sont pas utilisées dans l'implémentation
de leur table de hachage.



Pardon, j'ai confondu glib et libc...

La glib a bien des tables de hachage mais pas génériques, les fonctions
de hash dépendent de la nature de l'objet.



De quoi parles-tu ? J'ai trouvé hsearch et ses copines, qui fonctionne
juste pour des zero-terminated strings.



https://developer.gnome.org/glib/2.30/glib-Hash-Tables.html#GHashFunc

dit :

The functions g_direct_hash(), g_int_hash() and g_str_hash() provide
hash functions which can be used when the key is a gpointer, gint, and
gchar* respectively.



Je pense que ces fonctions sont proposées à l'utilisateur comme brique
de base pour faire ses propres fonctions de hash.

Marc Boyer
--
À mesure que les inégalités regressent, les attentes se renforcent.
François Dubet
Avatar
candide
Le 22/08/2013 10:56, Marc Boyer a écrit :

int hashX(X* x){
return hashmapHash( &(x->c) , 1 ) + hashmapHash( &(x->i), sizeof( x->i ) );
}




[Je reviens à ma petite obsession, mais nous sommes bien d'accord que
l'usage de hasmapHash pour hacher un int présente un petit risque,
n'est-ce pas ?]

Juste pour info, c'est une pratique courante que de faire la somme de
hashes pour obtenir un nouveau hash ?

Quelqu'un connaît l'usage pour hacher un point en 3D à coordonnées
entières (tableau de trois int) ? Pareil, on fait la somme ?
Avatar
Marc Boyer
Le 22-08-2013, candide a écrit :
Le 22/08/2013 10:56, Marc Boyer a écrit :

int hashX(X* x){
return hashmapHash( &(x->c) , 1 ) + hashmapHash( &(x->i), sizeof( x->i ) );
}




[Je reviens à ma petite obsession, mais nous sommes bien d'accord que
l'usage de hasmapHash pour hacher un int présente un petit risque,
n'est-ce pas ?]



Oui, tout à fait. Mais lever ce risque compliquerais un peu
le code.

Juste pour info, c'est une pratique courante que de faire la somme de
hashes pour obtenir un nouveau hash ?



C'est je pense assez courant, mais je ne sais pas si c'est une bonne
pratique en fait.

Quelqu'un connaît l'usage pour hacher un point en 3D à coordonnées
entières (tableau de trois int) ? Pareil, on fait la somme ?



La somme des 3 entiers ou la somme des trois fonctions de hashages.
La somme des 3 entiers n'est surement pas une bonne idée: c'est
surement pas bien repartit.

Marc Boyer
--
À mesure que les inégalités regressent, les attentes se renforcent.
François Dubet
Avatar
Alexandre Bacquart
On 08/22/2013 05:25 PM, Marc Boyer wrote:
Le 22-08-2013, candide a écrit :
Le 22/08/2013 10:56, Marc Boyer a écrit :

int hashX(X* x){
return hashmapHash(&(x->c) , 1 ) + hashmapHash(&(x->i), sizeof( x->i ) );
}




[Je reviens à ma petite obsession, mais nous sommes bien d'accord que
l'usage de hasmapHash pour hacher un int présente un petit risque,
n'est-ce pas ?]



Oui, tout à fait. Mais lever ce risque compliquerais un peu
le code.

Juste pour info, c'est une pratique courante que de faire la somme de
hashes pour obtenir un nouveau hash ?



C'est je pense assez courant, mais je ne sais pas si c'est une bonne
pratique en fait.

Quelqu'un connaît l'usage pour hacher un point en 3D à coordonnées
entières (tableau de trois int) ? Pareil, on fait la somme ?



La somme des 3 entiers ou la somme des trois fonctions de hashages.
La somme des 3 entiers n'est surement pas une bonne idée: c'est
surement pas bien repartit.



Moui, enfin si le hash d'entier se contente de retourner l'entier en
question (c'est souvent le cas), c'est kif-kif.

Le choix de l'algo de hachage peut devenir important pour certaines
utilisations. Pour des coordonnées 3D notament, on pourrait vouloir
faire des recherches de proximité => locality-sensitive hashing. C'est
juste un exemple...


--
Alexandre
1 2 3 4 5