题外话:为了尝鲜,也兴冲冲的安装了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,这个时候情况就更复杂,这个在后面一篇文章会综合各种情况更详细的说明。