web-dev-qa-db-fra.com

C ++ - Pourquoi boost :: hash_combine est la meilleure façon de combiner les valeurs de hachage?

J'ai lu dans d'autres articles que cela semble être la meilleure façon de combiner les valeurs de hachage. Quelqu'un pourrait-il s'il vous plaît décomposer cela et expliquer pourquoi c'est la meilleure façon de le faire?

template <class T>
inline void hash_combine(std::size_t& seed, const T& v)
{
    std::hash<T> hasher;
    seed ^= hasher(v) + 0x9e3779b9 + (seed<<6) + (seed>>2);
}

Edit: L'autre question ne demande que le nombre magique, mais je voudrais en savoir plus sur la fonction entière, pas seulement sur cette partie.

31
keyboard

C'est le "meilleur" qui est argumentatif.

C'est "bon", voire "très bon", du moins superficiellement, c'est facile.

seed ^= hasher(v) + 0x9e3779b9 + (seed<<6) + (seed>>2);

Nous supposerons que seed est un résultat précédent de hasher ou de cet algorithme.

^= Signifie que les bits de gauche et les bits de droite modifient tous les bits du résultat.

hasher(v) est supposé être un hachage décent sur v. Mais le reste est la défense au cas où ce ne serait pas un hachage décent.

0x9e3779b9 Est une valeur de 32 bits (elle pourrait être étendue à 64 bits si size_t Était de 64 bits sans doute) qui contient des demi-0 et des demi-1. Il s'agit essentiellement d'une série aléatoire de 0 et de 1 effectuée en approximant une constante irrationnelle particulière en tant que valeur de point fixe en base 2. Cela permet de garantir que si le hachage renvoie de mauvaises valeurs, nous obtenons toujours un frottis de 1 et de 0 dans notre sortie.

(seed<<6) + (seed>>2) Est un peu aléatoire de la graine entrante.

Imaginez que la constante 0x Était manquante. Imaginez que le hachage retourne la constante 0x01000 Pour presque chaque v passé. Maintenant, chaque bit de la graine est étalé sur la prochaine itération du hachage, au cours de laquelle il est à nouveau étalé.

La seed ^= (seed<<6) + (seed>>2)0x00001000 Devient 0x00041400 Après une itération. Alors 0x00859500. Lorsque vous répétez l'opération, tous les bits définis sont "étalés" sur les bits de sortie. Finalement, les bits droit et gauche entrent en collision et le déplacement déplace le bit défini des "emplacements pairs" aux "emplacements impairs".

Les bits qui dépendent de la valeur d'une graine d'entrée croissent relativement rapidement et de manière complexe à mesure que l'opération de combinaison se répète sur l'opération de graine. Ajouter des causes porte, ce qui macule encore plus les choses. La constante 0x Ajoute un tas de bits pseudo-aléatoires qui font que les valeurs de hachage ennuyeuses occupent plus de quelques bits de l'espace de hachage après avoir été combinées.

Il est asymétrique grâce à l'addition (la combinaison des hachages de "dog" Et "god" Donne des résultats différents), il gère les valeurs de hachage ennuyeuses (mappage des caractères à leur valeur ascii, ce qui implique seulement de tordre une poignée de bits ). Et, c'est assez rapide.

Des combinaisons de hachage plus lentes qui sont cryptographiquement solides peuvent être meilleures dans d'autres situations. Moi, naïvement, je présumerais que faire des décalages soit une combinaison de décalages pairs et impairs pourrait être une bonne idée (mais peut-être que l'ajout, qui déplace même les bits des bits impairs, en fait moins un problème: après 3 itérations, la graine solitaire entrante les bits entreront en collision et s'ajouteront et provoqueront un report).

L'inconvénient de ce type d'analyse est qu'il suffit d'une seule erreur pour rendre une fonction de hachage vraiment mauvaise. Souligner toutes les bonnes choses n'aide pas beaucoup. Donc, une autre chose qui le rend bon maintenant est qu'il est raisonnablement célèbre et dans un référentiel open-source, et je n'ai entendu personne expliquer pourquoi il est mauvais.

36

Ce n'est pas le meilleur, étonnamment pour moi ce n'est même pas particulièrement bon. entropy effect of a single bit change Figure 1: L'effet entropique d'un changement de bit unique dans l'un des deux nombres aléatoires de 32 bits combinés en un seul nombre de 32 bits en utilisant boost :: hash_combine

boost entropy matrix Figure 2: L'effet d'un changement de bit unique dans l'un des deux nombres aléatoires de 32 bits sur le résultat de boost :: hash_combine

L'effet entropique d'un changement de bit unique dans l'une ou l'autre valeur combinée doit être d'au moins log (2) [ligne noire]. Comme on peut le voir sur la figure 1, ce n'est pas le cas pour le bit le plus élevé de la valeur de départ et un peu serré pour la seconde à la valeur la plus élevée également. Cela signifie que statistiquement, les bits hauts de la graine sont perdus. En utilisant des rotations de bits au lieu de décalages de bits, xor ou addition avec report au lieu d'une simple addition, on pourrait facilement créer un hash_combine similaire qui préserve mieux l'entropie. De plus, lorsque le hachage et la graine sont tous deux à faible entropie, un hash_combine qui se propage davantage serait préférable. La rotation qui maximise cette propagation est la section dorée si le nombre de hachages à combiner n'est pas connu à l'avance ou est important. En utilisant ces idées, je propose le hash_combine suivant, qui utilise 6 opérations tout comme boost, mais réalise un comportement de hachage plus chaotique et préserve mieux les bits d'entrée. Bien sûr, on peut toujours se déchaîner et gagner le concours en ajoutant une seule multiplication par un entier impair, cela répartira très bien les hachages.

proposed entropy response Figure 3: L'effet entropique d'un changement de bit unique dans l'un des deux nombres aléatoires de 32 bits étant combiné à un seul nombre de 32 bits en utilisant l'alternative hash_combine proposée

proposed response matrix Figure 4: L'effet d'un changement de bit unique dans l'un des deux nombres aléatoires de 32 bits sur le résultat de l'alternative hash_combine proposée

#include <iostream>
#include <limits>
#include <cmath>
#include <random>
#include <bitset>
#include <iomanip>
#include "wmath.hpp"

using wmath::popcount;
using wmath::reverse;

using std::cout;
using std::endl;
using std::bitset;
using std::setw;


constexpr uint32_t hash_combine_boost(const uint32_t& a, const uint32_t& b){
  return a^( b + 0x9e3779b9 + (a<<6) + (a>>2) );
}

template <typename T,typename S>
typename std::enable_if<std::is_unsigned<T>::value,T>::type
constexpr rol(const T n, const S i){
  const T m = (std::numeric_limits<T>::digits-1);
  const T c = i&m;
  return (n<<c)|(n>>((-c)&m)); // this is usually recognized by the compiler to mean rotation, try it with godbolt
}

template <typename T,typename S>
typename std::enable_if<std::is_unsigned<T>::value,T>::type
constexpr ror(const T n, const S i){
  const T m = (std::numeric_limits<T>::digits-1);
  const T c = i&m;
  return (n>>c)|(n<<((-c)&m)); // this is usually recognized by the compiler to mean rotation, try it with godbolt
}

template <typename T>
typename std::enable_if<std::is_unsigned<T>::value,T>::type
constexpr circadd(const T& a,const T& b){
  const T t = a+b;
  return t+(t<a);
}

template <typename T>
typename std::enable_if<std::is_unsigned<T>::value,T>::type
constexpr circdff(const T& a,const T& b){
  const T t = a-b;
  return t-(t>a);
}

constexpr uint32_t hash_combine_proposed(const uint32_t&seed, const uint32_t& v){
  return rol(circadd(seed,v),19)^circdff(seed,v);
}

int main(){
  size_t boost_similarity[32*64]    = {0};
  size_t proposed_similarity[32*64] = {0};
  std::random_device urand;
  std::mt19937 mt(urand());
  std::uniform_int_distribution<uint32_t> bit(0,63);
  std::uniform_int_distribution<uint32_t> rnd;
  const size_t N = 1ull<<24;
  uint32_t a,b,c;
  size_t collisions_boost=0,collisions_proposed=0;
  for(size_t i=0;i!=N;++i){
    const size_t n = bit(mt);
    uint32_t t0 = rnd(mt);
    uint32_t t1 = rnd(mt);
    uint32_t t2 = t0;
    uint32_t t3 = t1;
    if (n>31){
      t2^=1ul<<(n-32);
    }else{
      t3^=1ul<<n;
    }
    a = hash_combine_boost(t0,t1);
    b = hash_combine_boost(t2,t3);
    c = a^b;
    size_t count = 0;
    for (size_t i=0;i!=32;++i) boost_similarity[n*32+i]+=(0!=(c&(1ul<<i)));
    a = hash_combine_proposed(t0,t1);
    b = hash_combine_proposed(t2,t3);
    c = a^b;
    for (size_t i=0;i!=32;++i) proposed_similarity[n*32+i]+=(0!=(c&(1ul<<i)));
  }

  for (size_t j=0;j!=64;++j){
    for (size_t i=0;i!=32;++i){
      cout << setw(12) << boost_similarity[j*32+i] << " ";
    }
    cout << endl;
  }

  for (size_t j=0;j!=64;++j){
    for (size_t i=0;i!=32;++i){
      cout << setw(12) << proposed_similarity[j*32+i] << " "; 
    }
    cout << endl;
  }
}

Modifier: dans le cas où vous souhaitez utiliser une fonction de hachage multiplicative, gardez à l'esprit qu'elle ne se répercute qu'en cascade sur les bits supérieurs. Mais cet inconvénient peut être compensé en utilisant des rotations binaires. Trois cycles de multiplications et de rotations consécutives (totalisant 6 opérations de base) semblent être suffisants pour produire des résultats très homogènes.

enter image description here

19
Lykos