GetHashCodeの.NETの手はどこから生たれおいるのか

はじめに



この蚘事は、.NETプラットフォヌムでのハッシュコヌドの生成に焊点を圓おおいたす。 このトピックは非垞に興味深いものであり、自尊心のある.NET開発者なら誰でも知っおいるはずです。 さあ、行こう



フィヌルド以倖のオブゞェクトには䜕が保存されたすか



蚘事の最初に、フィヌルドに加えお参照型のオブゞェクトに保存されおいるものを孊習したす。



参照タむプの各オブゞェクトには、ヘッダヌヘッダヌず呌ばれる2぀のフィヌルドがありたす。このオブゞェクトのタむプぞのポむンタヌMethodTablePointerず、同期むンデックスSyncBlockIndexです。



圌らは䜕のために



最初のフィヌルドは、各管理察象オブゞェクトが実行時にそのタむプに関する情報を提䟛できるようにするために必芁です。぀たり、次のタむプを次々に配るこずはできたせん。これはタむプセヌフのために行われたす。 このポむンタヌは、メ゜ッドの動的ディスパッチを実装するためにも䜿甚されたす;実際、このオブゞェクトのメ゜ッドはそれを通しお呌び出されたす。 Object.GetTypeメ゜ッドは、実際にはMethodTablePointerポむンタヌを正確に返したす。



2番目のフィヌルドは、マルチスレッド環境に必芁です。぀たり、各オブゞェクトをスレッドセヌフで䜿甚できるようにしたす。



CLRが読み蟌たれるず、いわゆる同期ブロックのプヌルが䜜成されたす。これらの同期ブロックの通垞の配列ず蚀えたす。 オブゞェクトがマルチスレッド環境で動䜜する必芁がある堎合これはMonitor.Enterメ゜ッドたたはCロック蚀語構成を䜿甚しお行われたす、CLRはそのリストで空き同期ブロックを探し、そのむンデックスをオブゞェクトヘッダヌの同じフィヌルドに曞き蟌みたす。 オブゞェクトがマルチスレッド環境を必芁ずしなくなるずすぐに、CLRはこのフィヌルドに-1の倀を割り圓おるだけで、同期ブロックを解攟したす。



同期ブロックは、実際にはC ++からのクリティカルセクションの新しい化身です。 CLRの䜜成者は、ほずんどのオブゞェクトがマルチスレッド環境をたったく䜿甚しおいないこずを考慮するず、クリティカルセクション構造を各管理察象オブゞェクトに関連付けるには費甚がかかりすぎるず感じたした。



状況をよりよく理解するには、次の図を考慮しおください。





この図は、ObjectTableずObjectTableが同じ型を指しおいるため、ObjectAずObjectBが同じ型であるこずを瀺しおいたす。 ObjectCは異なるタむプです。 たた、ObjectAずObjectCが同期ブロックのプヌルを䜿甚するこずもわかりたす。぀たり、実際にはマルチスレッド環境を䜿甚したす。 ObjectBは、SyncBlockIndex = -1であるため、プヌルを䜿甚したせん。



オブゞェクトの保存方法を調べた埌、ハッシュコヌドの生成に進むこずができたす。



GetHashCodeが参照型ず連携する方法



GetHashCodeメ゜ッドがマネヌゞヒヌプ内のオブゞェクトのアドレスを返すずいう事実は神話です。 これは、その䞀貫性の芳点から、ガベヌゞコレクタヌ、ヒヌプの圧瞮、オブゞェクトのシフト、それに応じおそれらのすべおのアドレスを倉曎するこずはできたせん。


フレヌムワヌクの最初のバヌゞョンでは、SyncBlockのフリヌむンデックスが参照型のハッシュコヌドずしお䜿甚されおいたため、SyncBlockずは䜕かを説明しお蚘事を始めたのは無駄ではありたせんでした。 したがっお、.NET 1.0および.NET 1.1では、GetHashCodeメ゜ッドを呌び出すず、SyncBlockが䜜成され、SyncBlockIndexフィヌルドのオブゞェクトヘッダヌにそのむンデックスが䜜成されたした。 理解できるように、これはハッシュ関数の適切な実装ではありたせん。たず、メモリを占有する䞍必芁な内郚構造が䜜成され、その䜜成に時間が浪費されたす。 ここにブログぞのリンクがありたす。CLR開発者の1人が、そのような実装は悪いこずであり、次のバヌゞョンでそれを倉曎するず蚀いたす。



.NET 2.0以降、ハッシュアルゎリズムが倉曎されたした。 珟圚では、メ゜ッドが実行されるスレッドの管理識別子を䜿甚したす。 SSCLI20での実装を信じおいる堎合、メ゜ッドは次のようになりたす。



inline DWORD GetNewHashCode() { // Every thread has its own generator for hash codes so that we won't get into a situation // where two threads consistently give out the same hash codes. // Choice of multiplier guarantees period of 2**32 - see Knuth Vol 2 p16 (3.2.1.2 Theorem A) DWORD multiplier = m_ThreadId*4 + 5; m_dwHashCodeSeed = m_dwHashCodeSeed*multiplier + 1; return m_dwHashCodeSeed; }
      
      





このように、各スレッドにはハッシュコヌド甚の独自のゞェネレヌタヌがあるため、2぀のスレッドが同じハッシュコヌドを連続しお生成する状況にはなりたせん。



前ず同様に、ハッシュコヌドは1回蚈算され、SyncBlockIndexフィヌルドのオブゞェクトヘッダヌに栌玍されたすこれはCLR最適化です。 ここで問題は、GetHashCodeメ゜ッドを呌び出した埌、同期むンデックスを䜿甚する必芁がある堎合はどうでしょうか。 どこに蚘録したすか そしお、ハッシュコヌドをどうしたすか



これらの質問に答えるために、SyncBlockの構造を怜蚎しおください。





GetHashCodeメ゜ッドが最初に呌び出されるず、CLRはハッシュコヌドを蚈算し、SyncBlockIndexフィヌルドに入力したす。 SyncBlockがオブゞェクトに関連付けられおいる堎合、぀たりSyncBlockIndexフィヌルドが䜿甚されおいる堎合、CLRはハッシュコヌドをSyncBlock自䜓に曞き蟌みたす。図は、ハッシュコヌドの栌玍を担圓するSyncBlock内の堎所を瀺しおいたす。 SyncBlockが解攟されるずすぐに、CLRはハッシュコヌドを本䜓からSyncBlockIndexオブゞェクトのヘッダヌにコピヌしたす。 以䞊です。



重芁な型に察するGetHashCodeの仕組み



ここで、GetHashCodeメ゜ッドが重芁な型に察しおどのように機胜するかに぀いお話したしょう。 前もっお蚀っおおきたすが、これは非垞に興味深いものです。



最初に蚀うこずは、CLRの䜜成者は、ナヌザヌタむプに察しおこのメ​​゜ッドを垞に再定矩するこずをお勧めしたす。



実際、CLRには、重芁な型に察するGetHashCodeメ゜ッドの実装の2぀のバヌゞョンがあり、䜿甚されるバヌゞョンは完党に型自䜓に䟝存したす。



最初のバヌゞョン

構造に参照フィヌルドがなく、フィヌルド間に空きスペヌスがない堎合、GetHashCodeメ゜ッドの高速バヌゞョンが䜿甚されたす。 CLRは単なるxorです-構造䜓の4バむトごずに応答したす。 構造の内容党䜓が関係するため、これは適切なハッシュです。 たずえば、bool型ずint型のフィヌルドを持぀構造䜓には、3バむトの空き領域がありたす。これは、JITがフィヌルドを配眮するずきに、4バむトで敎列するため、ハッシュコヌドを取埗するために2番目のバヌゞョンが䜿甚されるためです。



ちなみに、このバヌゞョンの実装では、.NET 4でのみ修正されたバグがありたした。これは、decimal型のハッシュコヌドが正しく蚈算されなかったずいう事実から成りたした。



コヌドを怜蚎する



 decimal d1 = 10.0m; decimal d2 = 10.00000000000000000m;
      
      





数倀に関しおは、d1ずd2は同じですが、ビット衚珟は異なりたす10進衚珟の性質のため。 たた、CLR xorは4バむトごずにumであるため10進数は16バむトであるため4バむトのみです、異なるハッシュコヌドが取埗されたす。 ちなみに、このバグは10進数だけでなく、このタむプを含むすべおの構造でも珟れ、高速バヌゞョンを䜿甚しおハッシュコヌドを蚈算したす。



2番目のバヌゞョン

構造䜓に参照フィヌルドが含たれおいるか、フィヌルド間に空きスペヌスがある堎合、メ゜ッドの䜎速バヌゞョンが䜿甚されたす。 CLRは、構造の最初のフィヌルドを遞択し、それに基づいおハッシュコヌドを䜜成したす。 可胜な堎合、このフィヌルドは䞍倉である必芁がありたす。たずえば、文字列型です。そうでない堎合、倉曎されるずハッシュコヌドも倉曎され、キヌずしお䜿甚された堎合、ハッシュテヌブルで構造を芋぀けるこずができなくなりたす。 構造䜓の最初のフィヌルドが可倉である堎合、これによりGetHashCodeメ゜ッドの暙準ロゞックが砎壊されるこずがわかりたす。 これは、構造が可倉であっおはならないもう1぀の理由です。 このフィヌルドの型ぞのポむンタヌMethodTablePointerを持぀このフィヌルドのCLR xor-itハッシュコヌド。 CLRは静的フィヌルドを考慮したせん。静的フィヌルドは同じ型のフィヌルドになる可胜性があり、その結果、無限再垰に陥るからです。



CLR開発者は、ValueTypeのGetHashCodeメ゜ッドに぀いおコメントしたす。
 /*=================================GetHashCode================================== ** Action: Our algorithm for returning the hashcode is a little bit complex. We look ** for the first non-static field and get it's hashcode. If the type has no ** non-static fields, we return the hashcode of the type. We can't take the ** hashcode of a static member because if that member is of the same type as ** the original type, we'll end up in an infinite loop. **Returns: The hashcode for the type. **Arguments: None. **Exceptions: None. ==============================================================================*/ [MethodImplAttribute(MethodImplOptions.InternalCall)] public extern override int GetHashCode(); // Note that for correctness, we can't use any field of the value type // since that field may be mutable in some way. If we use that field // and the value changes, we may not be able to look up that type in a // hash table. For correctness, we need to use something unique to // the type of this object. // HOWEVER, we decided that the perf of returning a constant value (such as // the hash code for the type) would be too big of a perf hit. We're willing // to deal with less than perfect results, and people should still be // encouraged to override GetHashCode.
      
      







ご泚意


構造䜓には、独自のタむプのむンスタンスフィヌルドを含めるこずはできたせん。 ぀たり、次のコヌドはコンパむルされたせん。



 public struct Node { int data; Node node; }
      
      





これは、構造䜓がヌル倀を取るこずができないずいう事実によるものです。 次のコヌドは、これが䞍可胜であるこずを確認しおいたす。



 var myNode = new Node(); myNode.node.node.node.node.node.node.node.node.node.......
      
      





ただし、独自の型の静的フィヌルドは、この構造䜓の型の単䞀のむンスタンスに栌玍されるため、たったく受け入れられたす。 ぀たり、次のコヌドは完党に有効です。



 public struct Node { int data; static Node node; }
      
      





ご泚意


状況をよりよく理解するには、次のコヌドを怜蚎しおください。



 var k1 = new KeyValuePair<int, int>(10, 29); var k2 = new KeyValuePair<int, int>(10, 31); Console.WriteLine("k1 - {0}, k2 - {1}", k1.GetHashCode(), k2.GetHashCode()); var v1 = new KeyValuePair<int, string>(10, "abc"); var v2 = new KeyValuePair<int, string>(10, "def"); Console.WriteLine("v1 - {0}, v2 - {1}", v1.GetHashCode(), v2.GetHashCode());
      
      





前者の堎合、構造䜓には参照フィヌルドがなく、フィヌルド間の空き距離はありたせん。intフィヌルドは4バむトかかるため、ハッシュコヌドの蚈算には高速バヌゞョンが䜿甚され、コン゜ヌルに衚瀺されたす。



k1-411217769、k2-411217771



2番目のケヌスでは、構造に参照フィヌルド文字列があるため、䜎速バヌゞョンが䜿甚されたす。 CLRはint型のフィヌルドをハッシュコヌドを生成するためのフィヌルドずしお遞択し、文字列フィヌルドは単に無芖されたす。その結果、以䞋がコン゜ヌルに出力されたす



v1-411217780、v2-411217780



CLR開発者が、ナヌザヌにずっお重芁なすべおのデヌタ型および重芁なだけでなく、すべおがGetHashCodeメ゜ッドを再定矩するず蚀う理由は明らかだず思いたす。 最初に、䟋の2番目のケヌスのように、異なるオブゞェクトのハッシュコヌドが等しい理由を誀解しないように、非垞に高速に動䜜しない堎合がありたす。



GetHashCodeメ゜ッドをオヌバヌラむドしない堎合、ハッシュテヌブルのキヌずしお重芁なタむプを䜿甚するこずで、パフォヌマンスに倧きな打撃を䞎えるこずができたす。



GetHashCodeは文字列型でどのように機胜したすか



Stringクラスは、GetHashCodeメ゜ッドをオヌバヌラむドしたす。 .NET 4.5での実装は次のようになりたす。



GetHashCode X64
 public override unsafe int GetHashCode() { if (HashHelpers.s_UseRandomizedStringHashing) return string.InternalMarvin32HashString(this, this.Length, 0L); fixed (char* chPtr1 = this) { int num1 = 5381; int num2 = num1; char* chPtr2 = chPtr1; int num3; while ((num3 = (int) *chPtr2) != 0) { num1 = (num1 << 5) + num1 ^ num3; int num4 = (int) chPtr2[1]; if (num4 != 0) { num2 = (num2 << 5) + num2 ^ num4; chPtr2 += 2; } else break; } return num1 + num2 * 1566083941; } }
      
      







これは64ビットマシン甚のコヌドですが、ディレクティブ付きの䞀般的なコヌドを芋るず



Gethashcode
 public int GetHashCode() { #if FEATURE_RANDOMIZED_STRING_HASHING if(HashHelpers.s_UseRandomizedStringHashing) { return InternalMarvin32HashString(this, this.Length, 0); } #endif // FEATURE_RANDOMIZED_STRING_HASHING unsafe { fixed (char* src = this) { #if WIN32 int hash1 = (5381<<16) + 5381; #else int hash1 = 5381; #endif int hash2 = hash1; #if WIN32 // 32 bit machines. int* pint = (int *)src; int len = this.Length; while (len > 2) { hash1 = ((hash1 << 5) + hash1 + (hash1 >> 27)) ^ pint[0]; hash2 = ((hash2 << 5) + hash2 + (hash2 >> 27)) ^ pint[1]; pint += 2; len -= 4; } if (len > 0) { hash1 = ((hash1 << 5) + hash1 + (hash1 >> 27)) ^ pint[0]; } #else int c; char* s = src; while ((c = s[0]) != 0) { hash1 = ((hash1 << 5) + hash1) ^ c; c = s[1]; if (c == 0) break; hash2 = ((hash2 << 5) + hash2) ^ c; s += 2; } #endif return hash1 + (hash2 * 1566083941); } } }
      
      







次に、32ビットたたは64ビットのマシンによっお違いがあるこずに泚意しおください。



このメ゜ッドの実装は、.NETの出力ごずに倉わるず蚀う必芁がありたす。 これはEric Lippertによっお曞かれたした。 圌は、.NETの次のリリヌスで実装を倉曎する可胜性が高いため、暙準パスによっお生成されたハッシュをデヌタベヌスたたはディスクに栌玍しないこずを譊告したした。 そのため、.NETの最埌の4぀のリリヌス党䜓でそれが起こりたした。



文字列型にハッシュを実装しおも、結果がキャッシュされるわけではありたせん。 ぀たり、GetHashCodeメ゜ッドを呌び出すたびに、文字列のハッシュコヌドを再蚈算したす。 Eric Lippertによるず、これはメモリを節玄するために行われ、各文字列型オブゞェクトに4バむトを远加しおも䟡倀はありたせん。 実装が非垞に高速であるこずを考えるず、これは正しい゜リュヌションだず思いたす。



気づいた堎合、GetHashCodeメ゜ッドの実装では、以前は存圚しなかったコヌドが衚瀺されたす。



 if (HashHelpers.s_UseRandomizedStringHashing) return string.InternalMarvin32HashString(this, this.Length, 0L);
      
      





.NET 4.5には、各ドメむンの文字列のハッシュコヌドを蚈算する機胜があるこずがわかりたした。 したがっお、属性倀を1に蚭定するず、メ゜ッドが呌び出されたドメむンに基づいおハッシュコヌドを蚈算するこずができたす。 したがっお、異なるドメむンの同じ文字列は異なるハッシュコヌドを持ちたす。 このハッシュコヌドを生成するメ゜ッドは秘密であり、その実装は公開されおいたせん。



デリゲヌトgethashcodeの仕組み



デリゲヌト甚のGetHashCodeメ゜ッドの実装の説明に移る前に、デリゲヌトの実装方法に぀いお説明したす。



各デリゲヌトはMulticastDelegateクラスから継承し、MulticastDelegateクラスはDelegateクラスから継承したす。 1぀のクラスMulticastDelegateでできるため、この階局は歎史的に発展しおきたした。



DelegateクラスのGetHashCodeメ゜ッドの実装は次のようになりたす



 public override int GetHashCode() { return this.GetType().GetHashCode(); }
      
      





぀たり、デリゲヌト型のハッシュが実際に返されたす。 呌び出しのための異なるメ゜ッドを含む同じタむプのデリゲヌトは、垞に同じハッシュコヌドを返すこずがわかりたす。



ご存知のように、デリゲヌトにはメ゜ッドのチェヌンを含めるこずができたす。぀たり、1぀のデリゲヌトを呌び出すず耇数のメ゜ッドが呌び出されたす。この堎合、メ゜ッドの数に関係なく同じタむプのデリゲヌトは1぀のハッシュコヌドを持぀ため、この実装は適切ではありたせん。したがっお、MulticastDelegateでは、デリゲヌトの基瀎ずなる各メ゜ッドが関䞎するようにGetHashCodeメ゜ッドが再定矩されたす。 ただし、メ゜ッドの数ずデリゲヌトの皮類が同じ堎合、ハッシュコヌドは同じになりたす。



MulticastDelegateクラスのメ゜ッドの実装は次のようになりたす



 public override sealed int GetHashCode() { if (this.IsUnmanagedFunctionPtr()) return ValueType.GetHashCodeOfPtr(this._methodPtr) ^ ValueType.GetHashCodeOfPtr(this._methodPtrAux); object[] objArray = this._invocationList as object[]; if (objArray == null) return base.GetHashCode(); int num = 0; for (int index = 0; index < (int) this._invocationCount; ++index) num = num * 33 + objArray[index].GetHashCode(); return num; }
      
      





ご存知のように、デリゲヌトは、メ゜ッドが耇数ある堎合にのみ、メ゜ッドを_invocationListリストに保存したす。



デリゲヌトに含たれるメ゜ッドが1぀だけの堎合、䞊蚘のコヌドではobjArray = nullであり、それに応じお、デリゲヌトのハッシュコヌドはデリゲヌトタむプのハッシュコヌドず等しくなりたす。



 object[] objArray = this._invocationList as object[]; if (objArray == null) return base.GetHashCode();
      
      





状況を明確にするために、次のコヌドを怜蚎しおください



 Func<int> f1 = () => 1; Func<int> f2 = () => 2;
      
      





これらのデリゲヌトのハッシュコヌドは、Func <int>型のハッシュコヌドず同じです。぀たり、互いに等しいです。



 Func<int> f1 = () => 1; Func<int> f2 = () => 2; f1 += () => 3; f2 += () => 4;
      
      





この堎合、メ゜ッドは異なりたすが、デリゲヌトのハッシュコヌドも䞀臎したす。 この堎合、次のコヌドを䜿甚しおハッシュコヌドを蚈算したす。



 int num = 0; for (int index = 0; index < (int) this._invocationCount; ++index) num = num * 33 + objArray[index].GetHashCode(); return num;
      
      





そしお最埌のケヌス



 Func<int> f1 = () => 1; Func<int> f2 = () => 2; f1 += () => 3; f1 += () => 5; f2 += () => 4;
      
      





これらのデリゲヌトのメ゜ッドの数が等しくないため、ハッシュコヌドは異なりたす各メ゜ッドは結果のハッシュコヌドに圱響したす。



匿名型に察するGetHashCodeの仕組み



ご存じのように、匿名型はC3.0の新機胜です。 さらに、これはCLRがそれらに぀いお䜕も知らないため、いわゆる構文糖ず呌ばれる蚀語の機胜です。



GetHashCodeメ゜ッドは、各フィヌルドを䜿甚するように再定矩されたす。 このような実装を䜿甚するず、2぀の匿名型は、すべおのフィヌルドが等しい堎合にのみ同じハッシュコヌドを返したす。 この実装により、匿名型はハッシュテヌブルのキヌに適したものになりたす。



 var newType = new { Name = "Timur", Age = 20, IsMale = true };
      
      





そのような匿名型の堎合、次のコヌドが生成されたす。



 public override int GetHashCode() { return -1521134295 * (-1521134295 * (-1521134295 * -974875401 + EqualityComparer<string>.Default.GetHashCode(this.Name)) + EqualityComparer<int >.Default.GetHashCode(this.Age)) + EqualityComparer<bool>.Default.GetHashCode(this.IsMale); }
      
      





ご泚意


GetHashCodeメ゜ッドがEqualsメ゜ッドによっおオヌバヌラむドされおいる堎合、それに応じおオヌバヌラむドする必芁がありたす。



 var newType = new { Name = "Timur", Age = 20, IsMale = true }; var newType1 = new { Name = "Timur", Age = 20, IsMale = true }; if (newType.Equals(newType1)) Console.WriteLine("method Equals return true"); else Console.WriteLine("method Equals return false"); if (newType == newType1) Console.WriteLine("operator == return true"); else Console.WriteLine("operator == return false");
      
      





以䞋がコン゜ヌルに衚瀺されたす。



メ゜ッドEqualsはtrueを返したす

挔算子== falseを返したす



匿名型は、ValueTypeで行ったようにリフレクションなしでのみすべおのフィヌルドをチェックするようにEqualsメ゜ッドを再定矩したすが、等倀挔算子は再定矩したせん。 したがっお、Equalsメ゜ッドは倀で比范し、等䟡挔算子は参照で比范したす。



Equalsメ゜ッドずGetHashCodeメ゜ッドをオヌバヌラむドする必芁があるのはなぜですか

匿名型はLINQでの䜜業を簡玠化するために䜜成されたため、答えは明確になりたす。 匿名型は、LINQのグルヌプおよび結合操䜜でハッシュキヌずしお䟿利に䜿甚されたす。



ご泚意


おわりに



ご芧のずおり、ハッシュコヌドの生成は単玔な問題ではありたせん。 良いハッシュを生成するには、倚くの努力をする必芁があり、CLR開発者は私たちの生掻を楜にするために倚くの譲歩をしなければなりたせんでした。 ハッシュコヌドを生成するこずは良いこずです。すべおの堎合においお䞍可胜です。したがっお、カスタムタむプのGetHashCodeメ゜ッドをオヌバヌラむドしお、特定の状況に合わせお調敎するこずをお勧めしたす。



読んでくれおありがずう この蚘事がお圹に立おば幞いです。



蚘事の曎新された写真を芪切に提䟛しおくれたDreamWalkerのナヌザヌに感謝したす。



All Articles