揭示同步塊索引(中):如何獲得對象的HashCode
2009-08-13 22:44 橫刀天笑 閱讀(6775) 評論(12) 收藏 舉報題外話:為了嘗鮮,也興沖沖的安裝了Win7,不過興奮之余卻郁悶不已,由于是用Live Writer寫博客,寫了好幾篇草稿,都完成了80%左右,沒有備份全部沒了。欲哭無淚,只好重寫了。
Visual Studio + SOS 小實驗
咋一看標題,覺得有些奇怪,同步塊索引和HashCode有啥關系呢。從名字上來看離著十萬八千里。在不知道細節之前,我也是這樣想的,知道細節之后,才發現這兩兄弟如此親密。我們還是先來用Visual Studio + SOS,看一個東西,下面是作為小白兔的示例代碼:
1: using System;
2: public class Program
3: {4: static void Main()
5: {6: Foo f = new Foo();
7: Console.WriteLine(f.GetHashCode()); 8: 9: Console.ReadLine(); 10: } 11: }12: //就這么一個簡單的類
13: public class Foo
14: { 15: 16: }(使用Visual Studio + SOS調試的時候,請先在項目的屬性,調試欄里設置“允許非托管代碼調試”)
我們分別在第7行,第9行設置斷點,F5運行,當程序停在第一個斷點處時(此時f.GetHashCode()還沒有執行),我們在Visual Studio的立即窗口里輸入:
1: .load sos.dll 2: extension C:\Windows\Microsoft.NET\Framework\v2.0.50727\sos.dll loaded 3: !dso 4: PDB symbol for mscorwks.dll not loaded 5: OS Thread Id: 0x1730 (5936) 6: ESP/REG Object Name 7: 0013ed78 01b72d58 Foo 8: 0013ed7c 01b72d58 Foo 9: 0013efc0 01b72d58 Foo 10: 0013efc4 01b72d58 Foo使用.load sos.dll加載sos模塊,然后使用!dso,我們找到了Foo類型的f對象的內存地址:01b72d58,然后使用Visual Studio調試菜單下的查看內存的窗口,查看f對象頭部的內容:
陰影遮住的00 00 00 00就是同步塊索引所在的地方了,可以看得出來,此時同步塊索引的值還是0(后面會對這個做解釋),然后繼續F5,程序運行到下一個斷點處,這個時候f.GetHashCode()也已調用了,細心的你就會發現,原來對象同步塊索引所在的地方的值變了:
Visual Studio這個內存查看器有個很好的功能,對內存變化的以紅色標出。我們看到,原來是00 00 00 00變成了現在的4a 73 78 0f。嗯,看來HashCode的獲取和同步塊索引還是有一些關系的,不然調用GetHashCode方法為什么同步塊索引的值會變化呢。再來看看Console.WriteLine(f.GetHashCode())的輸出:
不知道著兩個值有沒有什么關系,我們先把它們都換算成二進制吧。注意,這里的4a 73 78 0f是低位在左,高位在右,下面的十進制是高位再左,低位在右,那4a 73 78 0f實際上就是0x0f78734a了。
0x0f78734a:00001111011110000111001101001010
58225482:00000011011110000111001101001010
Rotor源代碼
我們先用0補齊32位,突然發現這兩者低26位居然是一模一樣的(紅色標出的部分),這是巧合還是必然?為了一探究竟只好搬出Rotor的源代碼,從源代碼里看看是否能發現什么東西。還是遵循老路子,我們先從托管代碼開始:
1: public virtual int GetHashCode()
2: {3: return InternalGetHashCode(this);
4: } 5: [MethodImpl(MethodImplOptions.InternalCall)]6: internal static extern int InternalGetHashCode(object obj);
在本系列的第一篇文章已經提到過,標記有[MethodImpl(MethodImplOptions.InternalCall)]特性的方法是使用Native Code的方式實現的,在Rotor中,這些代碼位于sscli20\clr\src\vm\ecall.cpp文件中:
1: FCFuncElement("InternalGetHashCode", ObjectNative::GetHashCode)
2: FCIMPL1(INT32, ObjectNative::GetHashCode, Object* obj) { 3: DWORD idx = 0; 4: OBJECTREF objRef(obj); 5: idx = GetHashCodeEx(OBJECTREFToObject(objRef));6: return idx;
7: } 8: FCIMPLEND 9: INT32 ObjectNative::GetHashCodeEx(Object *objRef) 10: {11: // This loop exists because we're inspecting the header dword of the object
12: // and it may change under us because of races with other threads.
13: // On top of that, it may have the spin lock bit set, in which case we're
14: // not supposed to change it.
15: // In all of these case, we need to retry the operation.
16: DWORD iter = 0;17: while (true)
18: { 19: DWORD bits = objRef->GetHeader()->GetBits(); 20: 21: if (bits & BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX)
22: {23: if (bits & BIT_SBLK_IS_HASHCODE)
24: {25: // Common case: the object already has a hash code
26: return bits & MASK_HASHCODE;
27: }28: else
29: {30: // We have a sync block index. This means if we already have a hash code,
31: // it is in the sync block, otherwise we generate a new one and store it there
32: SyncBlock *psb = objRef->GetSyncBlock(); 33: DWORD hashCode = psb->GetHashCode();34: if (hashCode != 0)
35: return hashCode;
36: 37: hashCode = Object::ComputeHashCode(); 38: 39: return psb->SetHashCode(hashCode);
40: } 41: }42: else
43: {44: // If a thread is holding the thin lock or an appdomain index is set, we need a syncblock
45: if ((bits & (SBLK_MASK_LOCK_THREADID | (SBLK_MASK_APPDOMAININDEX << SBLK_APPDOMAIN_SHIFT))) != 0)
46: { 47: objRef->GetSyncBlock();48: // No need to replicate the above code dealing with sync blocks
49: // here - in the next iteration of the loop, we'll realize
50: // we have a syncblock, and we'll do the right thing.
51: }52: else
53: { 54: DWORD hashCode = Object::ComputeHashCode(); 55: 56: DWORD newBits = bits | BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX | BIT_SBLK_IS_HASHCODE | hashCode; 57: 58: if (objRef->GetHeader()->SetBits(newBits, bits) == bits)
59: return hashCode;
60: // Header changed under us - let's restart this whole thing.
61: } 62: } 63: } 64: }代碼很多,不過大部分操作都是在做與、或、移位等。而操作的對象就是這行代碼獲取的:objRef->GetHeader()->GetBits(),實際上就是獲取同步塊索引。
想想,在第一個斷點命中的時候,同步塊索引的值還是0x00000000,那應該是下面這塊代碼執行:
1: DWORD hashCode = Object::ComputeHashCode(); 2: DWORD newBits = bits | BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX | BIT_SBLK_IS_HASHCODE | hashCode;3: if (objRef->GetHeader()->SetBits(newBits, bits) == bits)
4: return hashCode;
通過Object的ComputeHashCode方法算出一個哈希值來(由于本文不是關注哈希算法的,所以這里不討論這個ComputeHashCode方法的實現)。然后進行幾個或操作(這里還要與原先的bits或操作是為了保留原來的值,說明這個同步塊索引還起了別的作用,比如上篇文章的lock),然后將同步塊索引中老的位換掉。從這里我們還看不出來什么。不過,如果我們再次對這個對象調用GetHashCode()方法呢?那同步塊索引不再為0x00000000,而是0x0f78734a,在來看看幾個定義的常量的值:
1: #define BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX 0x08000000
2: #define BIT_SBLK_IS_HASHCODE 0x04000000
3: #define HASHCODE_BITS 26
4: #define MASK_HASHCODE ((1<<HASHCODE_BITS)-1)
從剛才設置hashcode的地方可以看到:DWORD newBits = bits | BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX | BIT_SBLK_IS_HASHCODE | hashCode;
所以開頭的兩個if都可以通過了,返回的hashcode就是bits & MASK_HASHCODE。
這個MASK_HASHCODE是將1向左移26位=100000000000000000000000000,然后減1=00000011111111111111111111111111(低26位全部為1,高6位為0),然后與同步塊索引相與,其實這里的作用不就是為了取出同步塊索引的低26位的值么。再回想一下本文開頭的那個試驗,原來不是巧合啊。
連上上一篇,我們可以看到同步塊索引不僅僅起到lock的作用,有時還承擔著存儲HashCode的責任。實際上同步塊索引是這樣的一個結構:總共32位,高6位作為控制位,后26的具體含義隨著高6位的不同而變化,高6位就像很多小開關,有的打開(1),有的關閉(0),不同位的打開和關閉有著不同的意義,程序也就知道低26位到底是干啥的了。這里的設計真是巧妙,不斷占用內存很緊湊,程序也可以靈活處理,靈活擴展。
后記
本篇和上一篇一樣,都是單獨將獨立的內容拿出來,這樣可以更簡單的來闡述。比如在本文中,我只設想同步塊索引做hashcode的存儲,這個時候,同步塊索引就干干凈凈(本文前面的試驗中先得到的同步塊索引就是一個0),但實際中同步塊索引可能擔任更多的職責,比如既lock,又要獲取HashCode,這個時候情況就更復雜,這個在后面一篇文章會綜合各種情況更詳細的說明。
浙公網安備 33010602011771號