同步块索引(中):如何获得对象的HashCode

时间:2022-08-19 16:47:24

题外话:为了尝鲜,也兴冲冲的安装了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对象头部的内容:

同步块索引(中):如何获得对象的HashCode

阴影遮住的00 00 00 00就是同步块索引所在的地方了,可以看得出来,此时同步块索引的值还是0(后面会对这个做解释),然后继续F5,程序运行到下一个断点处,这个时候f.GetHashCode()也已调用了,细心的你就会发现,原来对象同步块索引所在的地方的值变了:

同步块索引(中):如何获得对象的HashCode

Visual Studio这个内存查看器有个很好的功能,对内存变化的以红色标出。我们看到,原来是00 00 00 00变成了现在的4a 73 78 0f。嗯,看来HashCode的获取和同步块索引还是有一些关系的,不然调用GetHashCode方法为什么同步块索引的值会变化呢。再来看看Console.WriteLine(f.GetHashCode())的输出:

同步块索引(中):如何获得对象的HashCode 
不知道着两个值有没有什么关系,我们先把它们都换算成二进制吧。注意,这里的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,这个时候情况就更复杂,这个在后面一篇文章会综合各种情况更详细的说明。