web-dev-qa-db-ja.com

C ++-なぜboost :: hash_combineがハッシュ値を組み合わせる最良の方法なのですか?

私は他の投稿で、これがハッシュ値を組み合わせる最良の方法であると読んだことがあります。誰かがこれを分解して、なぜこれが最善の方法であるのかを説明してもらえますか?

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);
}

編集:他の質問はマジックナンバーを尋ねるだけですが、この部分だけでなく、機能全体について知りたいです。

31
keyboard

「最高」であることは議論の余地があります。

少なくとも表面的には、「良い」、または「非常に良い」ことは簡単です。

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

seedhasherまたはこのアルゴリズムの以前の結果であると想定します。

_^=_は、左側のビットと右側のビットがすべて結果のビットを変更することを意味します。

hasher(v)は、vの適切なハッシュであると推定されます。しかし、残りはまともなハッシュではない場合の防御です。

_0x9e3779b9_は32ビット値(_size_t_がほぼ64ビットである場合は64ビットに拡張できます)であり、ハーフ0とハーフ1を含んでいます。基本的に、特定の無理定数を基数2の固定小数点値として近似することにより行われる0と1のランダムなシリーズです。これにより、ハッシュが不正な値を返した場合でも、出力に1と0のスミアが残ることが保証されます。

_(seed<<6) + (seed>>2)_は、着信シードを少しシャッフルします。

_0x_定数が欠落していると想像してください。渡されたほぼすべてのvに対して、ハッシュが定数_0x01000_を返すことを想像してください。これで、シードの各ビットがハッシュの次の反復で展開され、その間に再び展開されます。

seed ^= (seed<<6) + (seed>>2) _0x00001000_は、1回の反復後に_0x00041400_になります。次に_0x00859500_。操作を繰り返すと、設定されたビットは出力ビットに「塗りつぶされ」ます。最終的に、右ビットと左ビットが衝突し、キャリーがセットビットを「偶数ロケーション」から「奇数ロケーション」に移動します。

入力シードの値に依存するビットは、結合操作がシード操作で繰り返されるため、比較的高速かつ複雑に成長します。原因を追加すると、物事がさらに不鮮明になります。 _0x_定数は、結合後、退屈なハッシュ値がハッシュ空間の数ビットより多くを占めるようにする一連の疑似ランダムビットを追加します。

追加(_"dog"_と_"god"_のハッシュを組み合わせることで異なる結果が得られる)のおかげで非対称であり、退屈なハッシュ値を処理します(文字をASCII値にマッピングします。そして、それはかなり速いです。

他の状況では、暗号的に強力な低速のハッシュ結合が適しています。私は、単純に、シフトを偶数シフトと奇数シフトの組み合わせにすることをお勧めします(ただし、奇数ビットから偶数ビットを移動する加算は、それほど問題になりません:3回の反復後、孤立したシードを入力しますビットが衝突して追加され、キャリーが発生します)。

この種の分析のマイナス面は、ハッシュ関数を本当に悪くするのにたった1つのミスしかかからないことです。すべての良いことを指摘しても、それほど役に立ちません。それで、今それを良くするもう一つのことは、それが合理的に有名で、オープンソースのレポジトリにあるということです。そして、私はそれがなぜ悪いのかを誰も指摘していない。

それは最高ではありませんが、驚くべきことに私にはそれは特に良くさえありません。 entropy effect of a single bit change 図1:boost :: hash_combineを使用して、2つのランダムな32ビット数の1つが単一の32ビット数に結合された場合の単一ビット変更のエントロピー効果

boost entropy matrix 図2:boost :: hash_combineの結果に対する2つのランダムな32ビット数の1つでの単一ビット変更の影響

結合されるいずれかの値の単一ビット変更のエントロピー効果は、少なくともlog(2)[黒線]である必要があります。図1からわかるように、これはシード値の最上位ビットの場合ではなく、2番目から最上位の値についても少しタイトです。これは、統計的にシードの上位ビットが失われていることを意味します。ビットシフトの代わりにビット回転を使用するか、単純な加算の代わりにキャリー付きのxorまたは加算を使用すると、エントロピーをよりよく保持する同様のhash_combineを簡単に作成できます。また、ハッシュとシードの両方のエントロピーが低い場合、より拡散するhash_combineが望ましいでしょう。結合されるハッシュの数が事前にわからない場合、または多い場合、この広がりを最大化する回転はゴールデンセクションです。これらのアイデアを使用して、次のhash_combineを提案します。これは、boostと同じように6つの操作を使用しますが、よりカオスなハッシュ動作を実現し、入力ビットをより良く保持します。もちろん、不均等な整数を1回乗算するだけで、いつでもワイルドになってコンテストに勝つことができます。これにより、ハッシュが非常にうまく分散されます。

proposed entropy response 図3:提案されたhash_combineの代替案を使用して、単一の32ビット数に結合される2つのランダムな32ビット数の1つにおける単一ビット変更のエントロピー効果

proposed response matrix 図4:提案されたhash_combineの代替案の結果に対する2つのランダムな32ビット数の1つでの1ビット変更の影響

#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;
  }
}

編集:乗法ハッシュ関数を使用する場合は、上位ビットにのみカスケードすることに注意してください。ただし、この欠点は、バイナリローテーションを使用して補うことができます。非常に均一な出力を生成するには、3ラウンドの連続した乗算と回転(合計6つの基本演算)で十分と思われます。

enter image description here

19
Lykos