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

Avatar
Benoit Izac
Bonjour,

le 22/08/2013 à 11:12, Marc Boyer a écrit dans le message
<kv4kma$mf4$ :

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é".



C'est possible de faire ça (mettre autre chose que 0 dans les 2 octets
non utilisés) ? Ça ne casse pas l'arithmétique des pointeurs ?

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.



Ce qui me gène c'est pourquoi, dans ce cas, memcmp prend en argument des
void* et non des char* ?

--
Benoit Izac
Avatar
candide
Le 22/08/2013 17:25, Marc Boyer a écrit :


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,




Entendu, je m'en inspirerai.



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 hashes bien sûr.
Avatar
candide
Le 22/08/2013 11:12, Marc Boyer a écrit :

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é".





Ah ok ! donc une même valeur entière pourrait facilement avoir deux
représentations différentes.


Bon, et si l'implémentation me dit, cf. gcc concernant les entiers :



-------------------------------------------
GCC supports only two’s complement integer types, and all bit patterns
are ordinary values.
-------------------------------------------



puis-je considérer qu'il y a bijection entre valeurs et représentations
? ce serait tellement plus simple !
Avatar
Samuel DEVULDER
Le 22/08/2013 16:42, 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 ?]

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




C'est un point important que beaucoup de monde néglige et qui peut
causer des bugs subtils (à cause de la rupture du contrat "equals" dans
la table de hachage) ou des performances déplorables à cause de
collisions trop nombreuses.

En fait tout si l'ordre de tes entiers compte ou pas, plus généralement
parlant si l'ordre de tes valeurs dans la donnée influence le résultat
du test "equals".

Si l’ordre n'importe pas, on a affaire à une sorte de Set (à la java),
auquel cas si on calcule le hashCode des tableaux {1,2,3} et {3,2,1} on
doit avoir la même valeur (car ces deux tableaux répondent equals() du
point de vu Set. Dans ce cas l'addition directe des hashCode de chaque
élement est parfait car l'addition est commutative.

En revanche si l'ordre importe (le 1er entier est de nature différente
du 2eme: ce qui est le cas typique dans une structure avec plusieurs
entiers), alors il est important que (1,2,3) ne renvoie pas le même
hashCode que (3,1,2) si on veut éviter de se payer une collision dans la
table de hachage. Dans ce cas on utilise une forumule du type:

h = hashCode(element1)
h = h*31 + hashCode(element2)
h = h*31 + hashCode(element3)

Remarque: Multiplier par un nombre premier est important pour éviter les
collisions par la suite.

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 ?



Tu as donc struct pt3d {int x, y, z;}

Cela semble nécessiter *absolument* un hashCoe qui tienne compte de
l'ordre des éléments (3 en x, n'est pas la même chose que 3 pour z).
Donc je ferais ainsi:

int h = 0;
h = h*31 + hashmapHash( &(point->x), sizeof( point->x ) );
h = h*31 + hashmapHash( &(point->y), sizeof( point->y ) );
h = h*31 + hashmapHash( &(point->z), sizeof( point->z ) );

Nota: dans beaucoup de structures de données, l'ordre des valeurs des
éléments importe beaucoup. Aussi tu trouvera souvent le "*31" dans la
littérature. C'est typiquement le cas quand on calcule le hashCode d'une
chaine de caractère: "abab" ne doit idéalement pas avoir le même
hash-code que "aabb" ou "bbaa".

sam.
Avatar
candide
Le 22/08/2013 21:24, Samuel DEVULDER a écrit :


C'est un point important que beaucoup de monde néglige et qui peut
causer des bugs subtils (à cause de la rupture du contrat "equals" dans
la table de hachage) ou des performances déplorables à cause de
collisions trop nombreuses.




Effectivement la somme étant "symétrique" et très "lisse", elle devrait
hacher peu et produire plus de collisions mais dans l'exemple de Marc,
le premier champ et le deuxième champ sont de types différents.



Remarque: Multiplier par un nombre premier est important pour éviter les
collisions par la suite.




Pas forcément, le hash de Bernstein multiplie par 33, cf.
http://www.strchr.com/hash_functions



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 ?



Tu as donc struct pt3d {int x, y, z;}

Cela semble nécessiter *absolument* un hashCoe qui tienne compte de
l'ordre des éléments (3 en x, n'est pas la même chose que 3 pour z).
Donc je ferais ainsi:

int h = 0;
h = h*31 + hashmapHash( &(point->x), sizeof( point->x ) );
h = h*31 + hashmapHash( &(point->y), sizeof( point->y ) );
h = h*31 + hashmapHash( &(point->z), sizeof( point->z ) );




OK je vois l'idée, c'est probablement mieux que de faire la somme (trop
symétrique) et finalement on reprend l'inusable (*) fonction de hachage
figurant dans mon message initial.

Merci de ta contribution.



(*) cf. KR2, chapitre 6, page 144
Avatar
Marc Boyer
Le 22-08-2013, Benoit Izac a écrit :
le 22/08/2013 à 11:12, Marc Boyer a écrit dans le message
<kv4kma$mf4$ :
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é".



C'est possible de faire ça (mettre autre chose que 0 dans les 2 octets
non utilisés) ?



Oui.

Ça ne casse pas l'arithmétique des pointeurs ?



Ben non.

Pour t'en convaincre, dis-toi que c'est la même chose avec les structures
et que l'arithmétique des pointeurs y marche très bien.

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.



Ce qui me gène c'est pourquoi, dans ce cas, memcmp prend en argument des
void* et non des char* ?



Je ne sais pas ;-)

void* et char* sont très proches, puisqu'ils doivent avoir même
représentation et mêmes contraintes d'alignement.

Mais on ne peut pas faire d'arithmétique des pointeurs sur void*,
ou déréférencer.

Il y a sûrement être des règles de conversion implicites différentes,
mais j'ai la flemme de chercher. Perso, void* me dit bien "mémoire
brute, pointeur de type inconnu, faites gaffe" alors que char*
me dit juste "pointeur sur char, ou tableau, ou chaine de caractère..."

Par exemple, gcc -Wall -pedantic couine un peu sur une convertion
implicite int* -> char* mais pas int* -> void*.

int i;
char* c= &i;
void* v= &i;

Marc Boyer
--
À mesure que les inégalités regressent, les attentes se renforcent.
François Dubet
Avatar
Jean-Marc Bourguet
candide writes:

puis-je considérer qu'il y a bijection entre valeurs et
représentations ?



Pour les entiers, les archi ou ce n'est pas le cas sont rarissimes.

(En fait, je ne vois comme candidats que les machines d'Unisys, ou il y
a des machines en complement a un -- et donc deux representations pour
0, je ne sais pas a quel point -0 est considere comme totalement
equivalent a +0 -- et des machines avec tags -- et je ne suis pas sur
que les bits qui ne participent pas a la valeur ne sont pas fixes)

A+

--
Jean-Marc
FAQ de fclc: http://www.levenez.com/lang/c/faq
Site de usenet-fr: http://www.usenet-fr.news.eu.org
Avatar
Tonton Th
On 2013-08-22, Marc Boyer wrote:

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.



Il me semble même que l'état "NaN" n'est encodé que sur quelques
bits de la représentation mémoire :)

--
http://weblog.mixart-myrys.org/?post/2013/07/Saucisse-de-geek
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Avatar
Jean-Marc Bourguet
Tonton Th writes:

On 2013-08-22, Marc Boyer wrote:

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.



Il me semble même que l'état "NaN" n'est encodé que sur quelques
bits de la représentation mémoire :)



Question interessante, est-ce que NaN est considere comme une valeur ou
plusieurs? De toute maniere le fait que x != x si x est un NaN offre
pas mal de possibilites de foutre le bazard si on cherche a les mettre
dans des conteneurs associatifs.

A+

--
Jean-Marc
FAQ de fclc: http://www.levenez.com/lang/c/faq
Site de usenet-fr: http://www.usenet-fr.news.eu.org
Avatar
Jean-Marc Bourguet
Jean-Marc Bourguet writes:

Tonton Th writes:

On 2013-08-22, Marc Boyer wrote:

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.



Il me semble même que l'état "NaN" n'est encodé que su r quelques
bits de la représentation mémoire :)



Question interessante, est-ce que NaN est considere comme une valeur ou
plusieurs?



Si j'ai bonne memoire, la norme definit assez precisement que c'est
l'argument qui est conserve (je ne sais plus ce qui se passe pour les
operations a plusieurs arguments)

--
Jean-Marc
FAQ de fclc: http://www.levenez.com/lang/c/faq
Site de usenet-fr: http://www.usenet-fr.news.eu.org