happyhippy

这个世界的问题在于聪明人充满疑惑,而傻子们坚信不疑。--罗素
posts - 137,comments - 602,trackbacks - 45


    前几天,我介绍了托管环境下struct实例的Layout和Size,其中介绍了StructLayoutAttribute特性,其实StructLayoutAttribute特性不只可以用在struct上,也可以用在class上,下面介绍下将StructLayoutAttribute运用在引用类型上时,对象实例的一些行为。

    在.net托管环境下,CRL像一个黑箱一样,将我们创建的对象丢在这个暗箱中进行操作,我们不能直接获得对象实例中字段的布局(Layout)和对象实例的size,但是我们可以通过Visual Studio+SOS扩展来进行非托管代码调试,并获得对象的这些信息。如果对非托管代码调试还不了解,可以参考我以前写的一篇《使用SOS - 在Visual Studio中启用非托管代码调试来支持本机代码调试》。

    默认情况下,C#编译器会在引用类型上运用[StructLayoutAttribute(LayoutKind.Auto)]特性,即按照CLR认为的最佳方式来排序实例中的字段顺序;当运用[StructLayout(LayoutKind.Sequential)]特性时,CLR会按照字段成员在被导出到非托管内存时出现的顺序依次布局,但我的测试结果是:貌似使用LayoutKind.Sequential与使用LayoutKind.Auto的结果相同;当运用[StructLayout(LayoutKind.Explicit)]时,我们可以自己设置实例中字段的位置。下面通过实例来进行分析:

//测试代码:
namespace Debug
{
    
class ClassAuto//C#编译器会自动在上面运用[StructLayout(LayoutKind.Auto)]
    {
        
public bool b1;  //1Byte
        public double d;//8byte
        public bool b2;  //1byte
    }


    [StructLayout(LayoutKind.Sequential)]
    
class ClassSeqt
    
{
        
public bool b1;  //1Byte
        public double d;//8byte
        public bool b2;  //1byte
    }


    [StructLayout(LayoutKind.Explicit)]
    
class ClassExpt1
    
{
        [FieldOffset(
0)]
        
public bool b1;  //1Byte
        [FieldOffset(0)]
        
public double d;//8byte
        [FieldOffset(8)]
        
public bool b2;  //1byte
    }


    [StructLayout(LayoutKind.Explicit)]
    
class ClassExpt2
    
{
        [FieldOffset(
0)]
        
public bool b1;  //1Byte
        [FieldOffset(0)]
        
public double d;//8byte
        [FieldOffset(0)]
        
public bool b2;  //1byte
    }


    
static class Program
    
{
        [STAThread]
        
static void Main()
        
{
            ClassAuto deft 
= new ClassAuto();
            ClassSeqt auto 
= new ClassSeqt();
            ClassExpt1 expt1 
= new ClassExpt1();
            ClassExpt2 expt2 
= new ClassExpt2();
            
return;  //注意:在这一行设置断点
        }

    }

}

 

    在进行测试之前,我先按照《托管环境下struct实例的Layout和Size》和《类型实例的创建位置、托管对象在托管堆上的结构》两篇文中的理论来猜测了下这四个对象的Size:
    CLR为每个对象添加SyncblkIndex和TypeHandle两个字段各占了4byte空间,合计8byte(有关SyncblkIndex和TypeHandle两个字段的讨论,可以参考《类型实例的创建位置、托管对象在托管堆上的结构》);
    对于在引用类型上应用默认的[StructLayout(LayoutKind.Auto)]特性情况,CLR应该会将两个bool型变量b1和b2放到相邻的位置中,因此ClassAuto的size应该是4(SyncblkIndex)+4(TypeHandle)+8(double d) + 1(bool b1) + 1(bool b2) + 2(4byte内存对齐)=20byte
    对于在引用类型上应用[StructLayout(LayoutKind.Sequential)]的情况,既然声明了布局顺序与声明顺序相同,则需要两个byte变量上的都要进行4byte的内存对齐,ClassSeqt的size应该是:4(SyncblkIndex)+4(TypeHandle)+1(bool b1) +3(4byte内存对齐) + 8(double d) + + 1(bool b2) + 3(4byte内存对齐)=24byte
    对于在引用类型上应用[StructLayout(LayoutKind.Explicit)]的情况,套用“在struct上应用[StructLayout(LayoutKind.Explicit)]时不会进行任何填充”的结论,ClassExpt1的size应该是4(SyncblkIndex)+4(TypeHandle)+8(double d,b1被d吃掉了) + 1(bool b2)=17byte;ClassExpt2的size应该是4(SyncblkIndex)+4(TypeHandle)+8(double d,b1和b2都被d吃掉了)=16byte
    我猜测这四个对象的内存布局图如下所示:



    上面仅仅只是我猜测的结果,但实际的测试结果并不完全是这样的,下面是测试过程:

    编译上面的代码,在Main函数的“return;”语句处设置断点,按F5进入Debug调试模式,程序运行到断点处中止;然后我们通过“菜单->Debug->Windows->Immediate”打开“Immediate Window”,在该窗口中先输入“.load sos”来启用非托管代码调试,提示已加载SOS.dll扩展后再输入“!DumpHeap -type Class”,此时会输出当前进程中类名中包含“Class”字符串的所有对象的信息,如下图所示:


    第一个表的第一列(Address)列出了这些对象的起始地址,第二列(MT)列出了类型的方法表(Method Table)的起始地址;第二个表的最后一列(Class Name)列出了这些对象的类型名,第一列(MT)列出了类型的方法表地址(跟上面这张表的第二列相同),第二列(Count)列出了当前进程中该对象实例的数量,第三列(Totel Size)内出了该类型的所有实例的总Size。下面就具体对各个对象进行分析:

1. ClassAuto:[StructLayout(LayoutKind.Auto)]

>!dumpobj 013e1a88
Name: Debug.ClassAuto
MethodTable: 00a530b8
EEClass: 00a51524
Size: 
20(0x14) bytes
 (E:\Project2005\Debug\Debug\bin\Debug\Debug.exe)
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
79104f64  
4000004        c       System.Boolean  0 instance        0 b1
791059c0  
4000005        4        System.Double  0 instance 0.000000 d
79104f64  
4000006        d       System.Boolean  0 instance        0 b2

    其size和Layout跟猜想中的一致(其中第三列Offset列出了该字段的偏移位置),注意,这里是按4byte进行内存对齐,而不是像struct一样按照成员的最大size进行对齐!

2. ClassSeqt:[StructLayout(LayoutKind.Sequential)]

>!dumpobj 013e1a9c
Name: Debug.ClassSeqt
MethodTable: 00a53150
EEClass: 00a51588
Size: 
20(0x14) bytes
 (E:\Project2005\Debug\Debug\bin\Debug\Debug.exe)
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
79104f64  
4000007        c       System.Boolean  0 instance        0 b1
791059c0  
4000008        4        System.Double  0 instance 0.000000 d
79104f64  
4000009        d       System.Boolean  0 instance        0 b2

    其size和Layout竟然跟使用[StructLayout(LayoutKind.Auto)]完全一致,也就是说,我上面猜测的24byte的布局是错误的,观察Offset列的值,我们可以看到,double d排在了第一个位置,bool b1排在了d的后面,也就是说CLR仍然按照[StructLayout(LayoutKind.Auto)]对字段进行布局。这是我在.net framework 2.0(Visual Studio 2005)上的测试结果,不知道其他版本的framework会不会是同样的结果-_-.

3. ClassExpt1:[StructLayout(LayoutKind.Explicit)]

>!dumpobj 013e1ab0
Name: Debug.ClassExpt1
MethodTable: 00a531e8
EEClass: 00a51648
Size: 
20(0x14) bytes
 (E:\Project2005\Debug\Debug\bin\Debug\Debug.exe)
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
79104f64  400000a        
4       System.Boolean  0 instance        0 b1
791059c0  400000b        
4        System.Double  0 instance 0.000000 d
79104f64  400000c        c       System.Boolean  
0 instance        0 b2

    Layout与我们在类型定义中的设定值一致;size为20byte,说明CLR对其进行了4byte的内存对齐;

4. ClassExpt2:[StructLayout(LayoutKind.Explicit)]

>!dumpobj 013e1ac4
Name: Debug.ClassExpt2
MethodTable: 00a53280
EEClass: 00a51708
Size: 
16(0x10) bytes
 (E:\Project2005\Debug\Debug\bin\Debug\Debug.exe)
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
79104f64  400000d        
4       System.Boolean  0 instance        0 b1
791059c0  400000e        
4        System.Double  0 instance 0.000000 d
79104f64  400000f        
4       System.Boolean  0 instance        0 b2

   其size和Layout跟猜想中的一致。
   四个对象的实际Layout如下图所示(根据上面表中的Offset来排的):


    最后补充一点和struct一样要注意的地方:如果在运用了[StructLayout(LayoutKind.Explicit)],计算FieldOffset一定要小心,例如我们使用上面ClassExpt1来进行下面的测试:

ClassExpt1 expt1 = new ClassExpt1();
expt1.d 
= 0;
expt1.b1 
= true;
Console.WriteLine(expt1.d);

    输出的结果不再是0了,而是4.94065645841247E-324,这是因为expt1.b1和expt1.d共享了一个byte,执行“expt1.b1 = true;时”也改变了expt1.d,CPU在按照浮点数的格式解析expt1.d时就得到了这个结果。(有关浮点数讨论可以参考我以前写的《精确判断一个浮点数是否等于0》)。所以在运用LayoutKind.Explicit时千万别把FieldOffset算错了:)


   结论:在32位的计算机上,默认情况下,对于引用类型的实例,CLR总是按4byte进行内存对齐

posted on 2007-04-17 16:42  Silent Void  阅读(...)  评论(... 编辑 收藏