小説の前に謝罪。自由に提案したり、直接編集したりしてください。--チェット
オーバーフローがありますが、例外ではありません。
危険は精度を失うことではなく、射程距離を失うことから来ます。「素数」が 2 の大きなべき乗であり、簡潔にするために 8 ビットの符号なし数値である、ばかげた例を使用してみましょう。(hash1 ^ hash2)
そして、それが 255であると仮定します。
"prime": 1000 0000
(hash1 ^ hash2): 1111 1111
角かっこで切り捨てられた数字を示すと、結果は次のようになります。
product: [0111 1111] 1000 0000
ただし、128 を掛けることは、左に 7 桁シフトすることと同じです。(hash1 ^ hash2)
したがって、 の値が何であれ、積の最下位の場所には 7 つのゼロがあることがわかります。したがって、(hash1 ^ hash2)
が奇数 (最下位ビット = 1) の場合、128 を掛けた結果は常に 128 になります (上位桁を切り捨てた後)。が偶数の場合(hash1 ^ hash2)
(LSB が 0 の場合、積は常にゼロになります。
これは、より大きなビットサイズに拡張されます。一般的なポイントは、「prime
」の下位ビットがゼロの場合、下位ビットにゼロを与えるシフト(または複数シフト+合計)操作を行っているということです。そして、乗算の積の範囲が損なわれます。
prime
しかし、最下位ビットが常に 1 になるように" " を奇数にしてみましょう。これをシフト / 加算操作に分解することを考えてください。のシフトされていない値は、(hash1 ^ hash2)
常に被加数の 1 つになります。偶数 " prime
" 乗数によって保証された無用にシフトされた最下位ビットは、少なくとも元の値のビットに基づいて設定され(hash1 ^ hash2)
ます。
prime
さて、実際に素数である値を考えてみましょう。2 より大きい場合は、奇数であることがわかります。したがって、下位ビットは無駄にシフトされていません。また、十分に大きな素数を選択することで、小さい素数を使用する場合よりも、出力値の範囲全体でより適切な分布が得られます。
0010 0000 1111 1011
8443 ( ) と 59 ( )を使用した 16 ビット乗算の演習を試してください0000 0000 0011 1011
。それらは両方とも素数であり、59 の下位ビットは 65531 の下位ビットと一致します。たとえば、hash1 と hash2 が両方とも ASCII 文字値 (0 .. 255) の場合、(hash1 ^ hash2) * のすべての結果59 は <= 15045 になります。これは、16 ビットの数値のハッシュ値の範囲 (0..65535) の約 1/4 が使用されないことを意味します。
しかし(hash1 ^ hash2) * 8443
、マップ全体にあります。が 8 のように小さい場合はオーバーフロー(hash1 ^ hash2)
します。非常に小さい入力数値でも 16 ビットすべてを使用します。入力数値が比較的小さい範囲にある場合でも、範囲全体でハッシュ値のクラスター化ははるかに少なくなります。
オーバーフローが問題ではないと仮定すると (JVM は自動キャストを実行します)、キャストの代わりにビットシフトを実行する方がよいでしょうか?
ほとんどの場合、そうではありません。とにかく、JVM はホスト プロセッサ上で効率的な実装に変換する必要があります。整数乗算はハードウェアに実装する必要があります。そうでない場合、JVM は操作を CPU にとって妥当なものに変換する責任があります。整数乗算の場合は、すでに高度に最適化されている可能性が非常に高いです。特定の CPU で整数乗算がシフトアンド加算としてより高速に実行される場合、JVM はそれをそのように実装する必要があります。しかし、JVM を作成している人々が、複数のシフトと加算の操作が 1 つの整数乗算に結合された可能性があるケースに気を配る可能性は低いでしょう。
ハッシュコード関数のパフォーマンスは、ハッシュコードの複雑さによって大きく異なると思います。素数乗数のサイズはパフォーマンスに影響しませんか?
いいえ。ハードウェアで実行する場合、サイズや設定されたビット数などに関係なく、操作は同じです。おそらく数クロック サイクルです。特定の CPU によって異なりますが、入力値に関係なく一定時間の操作になるはずです。
単一の乗数ではなく、カスタム ハッシュコード関数で複数の素数を使用する方が良い/スマート/高速ですか? そうでない場合、他の利点はありますか?
衝突の可能性を減らす場合にのみ、これは使用している数値によって異なります。ハッシュ コードが と に依存してA
おりB
、それらが同じ範囲にある場合は、異なる素数を使用するか、入力値の 1 つをシフトして、ビット間のオーバーラップを減らすことを検討してください。値に直接依存するのではなく、個々のハッシュ コードに依存しているため、ハッシュ コードが適切な分散などを提供すると想定するのが合理的です。
のハッシュコード(x, y)
を(y, x)
. ハッシュ関数がA
とB
を同じように扱う場合、 hash(x, y) = hash(y, x)
. それが必要な場合は、必ず同じ乗数を使用してください。そうではありません。別の乗数を使用することは理にかなっています。
のようなものlong lhash = prime * (hash1 ^ hash2);
はどう(int)((lhash >> 32) ^ lhash)
ですか?それは私が別の質問で見たものですSOですが、なぜそのようにするのが良い考えなのかは実際には説明されていませんでした.
興味深い質問です。Java では、long は 64 ビットで、int は 32 ビットです。したがって、これは必要なビット数の 2 倍を使用してハッシュを生成し、上位ビットと下位ビットを組み合わせて結果を導き出します。
n
数値を素数で乗算しp
、 の最下位k
ビットn
がすべてゼロの場合k
、積の最下位ビットn * p
もすべてゼロになります。これは非常に簡単にわかります。たとえば、n = 0011 0000
と を乗算している場合p = 0011 1011
、積は 2 つのシフト操作の和として表すことができます。または、
00110000 * p = 00100000 * p + 00010000 * p
= p << 5 + p << 4
p = 59
unsigned 8 ビット int と 16 ビット long を取得して使用する例を次に示します。
64: 0011 1011 * 0100 0000 = [ 0000 1110 ] 1100 0000 (192)
128: 0011 1011 * 1000 0000 = [ 0001 1101 ] 1000 0000 (128)
192: 0011 1011 * 1100 0000 = [ 0010 1100 ] 0100 0000 (64)
結果の上位ビットを削除するだけで、非素数被乗数の下位ビットがすべてゼロの場合、結果のハッシュ値の範囲が制限されます。それが特定のコンテキストの問題であるかどうかは、コンテキスト固有です。ただし、一般的なハッシュ関数の場合、入力数値にパターンがある場合でも、出力値の範囲を制限しないようにすることをお勧めします。また、セキュリティ アプリケーションでは、出力のパターンに基づいて元の値を推測できるようなものを避けることがさらに重要です。下位ビットを取得するだけで、元のビットの一部の正確な値が明らかになります。入力数値に大きな素数を乗算する操作が含まれていると仮定すると、元の数値にはハッシュ出力と同じ数のゼロが右側にあることがわかります (素数'
上位ビットと下位ビットを XOR することにより、出力の一貫性が低下します。さらに重要なことに、この情報に基づいて入力値を推測することははるかに困難です。XOR の仕組みに基づいて、元の下位ビットが 0 で上位ビットが 1 であったか、元の下位ビットが 1 で上位ビットが 0 であったことを意味する可能性があります。
64: 0011 1011 * 0100 0000 = 0000 1110 1100 0000 => 1100 1110 (206)
128: 0011 1011 * 1000 0000 = 0001 1101 1000 0000 => 1001 1101 (157)
192: 0011 1011 * 1100 0000 = 0010 1100 0100 0000 => 0110 1100 (204)