Hello World
Spiga

相同类型的每个对象大小都是一样的吗?

2009-11-30 14:09 by 老赵, 4585 visits

快速回答:“相同(引用)类型的每个对象大小都是一样的吗?”其实个问题对于大多数情况下来说应该正确的,不过的确也有些类型受到CLR的特殊照顾,因而有那么些例外。我现在尝试使用一些简单的小实验来进行验证,当然它是不严谨的,只能算是一个简单尝试而已。

比如,我们有这么一个类型:

public class SomeClass
{
    public int Field;
}

然后编写这样的代码:

var c1 = new SomeClass();
var c2 = new SomeClass();
var c3 = new SomeClass();
var c4 = new SomeClass();
var c5 = new SomeClass();

GC.Collect(2);

unsafe
{
    fixed (int* ptr1 = &c1.Field,
        ptr2 = &c2.Field,
        ptr3 = &c3.Field,
        ptr4 = &c4.Field,
        ptr5 = &c5.Field)
    {
        Console.WriteLine("Size of c1: " + ((int)ptr2 - (int)ptr1));
        Console.WriteLine("Size of c2: " + ((int)ptr3 - (int)ptr2));
        Console.WriteLine("Size of c3: " + ((int)ptr4 - (int)ptr3));
        Console.WriteLine("Size of c4: " + ((int)ptr5 - (int)ptr4));
    }
}

运行这段代码的结果是:

Size of c1: 12
Size of c2: 12
Size of c3: 12
Size of c4: 12

当然,如果我们要得出“每个SomeClass对象大小是12字节”,那么还必须有如下两个前提

  • 在堆中分配对象时是连续的。
  • 相同类型的实例,内部字段地布局(或者说“顺序”)是相同的。

但是,有一个类型可能是个特例,那就是我们随处可见的String类型。为此,我们再写一段代码进行实验:

static void Main()
{
    var c1 = new SomeClass();
    var s10 = new String('a', 10);
    var c2 = new SomeClass();
    var s20 = new String('a', 20);
    var c3 = new SomeClass();
    var s30 = new String('a', 30);
    var c4 = new SomeClass();
    var s40 = new String('a', 40);
    var c5 = new SomeClass();

    GC.Collect(2);

    unsafe
    {
        fixed (int* ptr1 = &c1.Field,
            ptr2 = &c2.Field,
            ptr3 = &c3.Field,
            ptr4 = &c4.Field,
            ptr5 = &c5.Field)
        {
            Console.WriteLine("Size of s10: " + ((int)ptr2 - (int)ptr1 - 12));
            Console.WriteLine("Size of s20: " + ((int)ptr3 - (int)ptr2 - 12));
            Console.WriteLine("Size of s30: " + ((int)ptr4 - (int)ptr3 - 12));
            Console.WriteLine("Size of s40: " + ((int)ptr5 - (int)ptr4 - 12));
        }
    }

    DoSomething(s10, s20, s30, s40);

    Console.ReadLine();
}

private static void DoSomething(params object[] args)
{
    // nothing
}

DoSomething的作用仅仅是为了避免s10-40几个字符串直接被当作垃圾而释放掉。上面这段代码中可以得出:

Size of s10: 40
Size of s20: 60
Size of s30: 80
Size of s40: 100

这个结果似乎是说:长度为10的字符串占40字节,长度为20的字符串占60字节……以此类推,长度为n的字符串占20 + 2n个字节(这也说明CLR中的字符是使用双字节的Unicode进行存储)。

真的是这样吗?事实上这个实验中并不能严格得出“不同长度String对象大小不同”,因为,万一String类型也只是直接创建了一个字符数组呢?这样,其实每个String对象的大小还是相同的,大小不同的只是字符数组而已。那么究竟事实是怎么样的呢?这只能靠WinDBG + SOS来一探究竟了,有空我再试试看。

而现在,我也只能靠猜的。我猜String对象是在自身内部包含了一长串字符,并非引用了一个字符数组,因为您看它的构造函数其实都是extern的,要靠外部调用的东东其中一定有猫腻:

[MethodImpl(MethodImplOptions.InternalCall)]
public extern String(char[] value);
[MethodImpl(MethodImplOptions.InternalCall), CLSCompliant(false)]
public extern unsafe String(char* value);
[MethodImpl(MethodImplOptions.InternalCall), CLSCompliant(false)]
public extern unsafe String(sbyte* value);
[MethodImpl(MethodImplOptions.InternalCall)]
public extern String(char c, int count);
[MethodImpl(MethodImplOptions.InternalCall), CLSCompliant(false)]
public extern unsafe String(char* value, int startIndex, int length);
[MethodImpl(MethodImplOptions.InternalCall), CLSCompliant(false)]
public extern unsafe String(sbyte* value, int startIndex, int length);
[MethodImpl(MethodImplOptions.InternalCall)]
public extern String(char[] value, int startIndex, int length);
[MethodImpl(MethodImplOptions.InternalCall), CLSCompliant(false)]
public extern unsafe String(sbyte* value, int startIndex, int length, Encoding enc);

最后回到标题:“相同(引用)类型的每个对象大小都是一样的吗?”答案肯定是否定的咯。最简单的例子便是int数组,不同长度的int数组类型相同,但大小明显不一样。当然,无论什么类型的数组都这样。

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

41 条回复

  1. AutumnWinter
    *.*.*.*
    链接

    AutumnWinter 2009-11-30 14:18:00

    这篇和上一篇都很水,希望看到你更高层次的作品!

  2. 老赵
    admin
    链接

    老赵 2009-11-30 14:32:00

    @AutumnWinter
    水吗?不过从评论上看,会知道很多东西呢。
    想到啥写啥这才叫博客嘛。
    你喜欢啥样的文章,比如我以前哪些文章?

  3. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-11-30 14:41:00

    Jeffrey Zhao:
    想到啥写啥这才叫博客嘛。


    嗯,同意。我觉得写篇很牛X的文章把所有结论都说得很清楚确实不错,但写些简单的、可操作的探索步骤也很有趣。如果不知道别人是如何得知结论的,自己就很难学习探索的方法,那就很难到自己探索到新东西了。

    老赵下次需要避免某个引用被优化而提前被当垃圾给收集掉用可以用GC.KeepAlive()……不用自己专门写方法来挂住对象。而且不加[MethodImpl(MethodImplOptions.NoInlining)]的这种简单(直线型)小方法很容易在实际执行的时候被inline掉,实际上未必能达到挂住对象的目的。GC.KeepAlive()是在JIT后方法的“元数据”里保存方法内引用信息的表里把引用的“活动”范围扩大了,很直接。

  4. AutumnWinter
    *.*.*.*
    链接

    AutumnWinter 2009-11-30 14:44:00

    Jeffrey Zhao:
    @AutumnWinter
    水吗?不过从评论上看,会知道很多东西呢。
    想到啥写啥这才叫博客嘛。
    你喜欢啥样的文章,比如我以前哪些文章?


    要说实话啊,我最喜欢你你那些非技术类的评论和观点,比如思考讨论系列里面的一些文章

  5. 老赵
    admin
    链接

    老赵 2009-11-30 14:47:00

    @RednaxelaFX
    你是从哪些地方知道GC.KeepAlive()的啊?那么,我这段代码是不是可以改成这样呢?

    var c1 = new SomeClass();
    GC.KeepAlive(new String('a', 10));
    var c2 = new SomeClass();
    GC.KeepAlive(new String('a', 20));
    var c3 = new SomeClass();
    GC.KeepAlive(new String('a', 30));
    var c4 = new SomeClass();
    GC.KeepAlive(new String('a', 40));
    var c5 = new SomeClass();

    从结果上看是OK的。

  6. 老赵
    admin
    链接

    老赵 2009-11-30 14:49:00

    AutumnWinter:
    要说实话啊,我最喜欢你你那些非技术类的评论和观点,比如思考讨论系列里面的一些文章


    hoho……那就没办法了,那些只有很少才能写个一篇两篇的……

  7. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-11-30 14:58:00

    @Jeffrey Zhao
    GC.KeepAlive()的作用是:你把它放在代码中的哪个点上,它就保证其参数所指向的对象在那个点之前都是活的。所以下面两段代码的保证是不一样的:

    var o = new object();
    GC.KeepAlive(o);
    
    // do something


    var o = new object();
    // do something
    
    GC.KeepAlive(o);

    当然如果你的目的只是在C#编译器编译出来的MSIL里看到那个对象相关代码还存在的话,在方法里随便什么地方写GC.KeepAlive()都可以。

    这个方法在MSDN里有文档。当然它具体是怎么实现的这个还是观察很多代码总结出来的……所以我不保证我的理解正确,就像测试没办法用于证明程序正确一样 =_=|||

  8. 老赵
    admin
    链接

    老赵 2009-11-30 15:04:00

    @RednaxelaFX
    汗,那么我怎么觉得它就像是一个空方法(比如我的加了NoInline的DoSomething方法),最多只是JIT给它开了个后门,不对它进行优化,或者说进行一些特别的优化而已……

  9. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-11-30 15:04:00

    @Jeffrey Zhao
    话说CLR的JIT把MSIL编译为native code之后,其实得到了好几块相关的东西:
    1、实现源程序逻辑的native code
    2、异常表,用于将异常处理映射到代码中的offset上
    3、引用表,用于记录方法的native code中什么范围内那些寄存器/栈上变量存有指针

    GC.KeepAlive()会影响到1和3:影响到实际的native code是因为有些优化做不了了;影响到引用表是在里面记录的某个指针的存活范围加大了。

  10. 老赵
    admin
    链接

    老赵 2009-11-30 15:15:00

    @RednaxelaFX
    继续求教RFX大仙是如何知道这些的……

  11. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-11-30 15:23:00

    Jeffrey Zhao:
    @RednaxelaFX
    汗,那么我怎么觉得它就像是一个空方法(比如我的加了NoInline的DoSomething方法),最多只是JIT给它开了个后门,不对它进行优化,或者说进行一些特别的优化而已……


    你写的DoSomething()是一个普通的managed方法,JIT对它不会有特别处理;GC.KeepAlive()可是InternalCall,实现是runtime提供的,做了手脚也不奇怪吧?(当然,其实它里面几乎就是空的……你可以把它看成是给JIT提示用的标记方法)。

    我……我读过SSCLI的代码,知道SSCLI与CLR共通的一些数据结构;然后我也做过很多实验看各种情况下JIT出来的代码的状况,包括内存中native code前后存的数据;然后我也调试到CLR里看了看……就这样而已。

    Jeffrey Zhao:
    @RednaxelaFX
    继续求教RFX大仙是如何知道这些的……


    这个……不要越来越过分了哈,大牛就已经很那啥了还老神仙大仙的 orz
    我就一刚毕业的小屁孩而已 TvT

  12. winter-cn
    *.*.*.*
    链接

    winter-cn 2009-11-30 15:23:00

    Jeffrey Zhao:
    @AutumnWinter
    水吗?不过从评论上看,会知道很多东西呢。
    想到啥写啥这才叫博客嘛。
    你喜欢啥样的文章,比如我以前哪些文章?


    嗯 我也同意 不过最近真的很水 老赵最近经常想到很水的东西......
    哈哈哈

  13. 老赵
    admin
    链接

    老赵 2009-11-30 15:30:00

    @RednaxelaFX
    话说SSCLI是我一直想看却一直没下决心看的东西,人笨就是没办法……
    再问一个问题,你是怎么知道哪些部分在SSCLI和CLR里是相同的啊?
    理论上JIT之后的结果,以及JIT的实现都是没有标准的吧。

  14. 老赵
    admin
    链接

    老赵 2009-11-30 15:39:00

    @winter-cn
    那个,到底啥叫水呀……

  15. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-11-30 15:46:00

    @Jeffrey Zhao
    所以说我观察了内存里的数据撒。没有任何外部文档会告诉你SSCLI什么地方跟CLR是一样的。但你可以调试CLR看里面用的数据结构的样子跟SSCLI源码里所描述的是否一致。SSCLI的JIT和GC实现跟CLR的都非常不同;前者的被极度简化了。但有些作为接口用的数据结构却是一致的。所以……本来直接调试CLR也可以知道很多东西,有SSCLI源码使得猜测可以在较小范围内进行,加快了调试的进度,仅此而已。

    关于SSCLI与CLR的不同,举个简单的例子:
    SSCLI(包括2.0)中的JIT是个简单的macro code emitter,优化非常少,相比之下CLR的JIT则复杂许多。在保持数据时它们也有不同之处:SSCLI编译出代码后,在MethodTable里始终存的是个stub,为了能够在GC把不常用的JIT后native code给回收掉之后,能够有机会再次触发JIT编译;当前CLR的JIT生成代码后则是直接把原本的stub替换为native code的entry point,于是比SSCLI少一层间接,但也失去了抛弃代码(code pitch)的能力。
    据说以后CLR还是有可能把code pitch功能加进来的,到时候CLR用的数据结构又会与现在的不同,而这是任何外部文档都不会告诉你的,只能靠自己去探索,或者找知情的人问问。

  16. Sunny Peng
    *.*.*.*
    链接

    Sunny Peng 2009-11-30 16:01:00

    虽然不做.net了,但这种文章还是有意义的。我也喜欢对一些小问题进行思考,不过接触面没那么广,随笔还是随着自己的感觉和思想写,各个都去写大篇章,讲大道理是不现实的,再说有些小知识还是能体现大道理的。

  17. 韦恩卑鄙 alias:v-zhewg
    *.*.*.*
    链接

    韦恩卑鄙 alias:v-zhewg 2009-11-30 16:20:00


    相同类型的每个对象大小都是一样的吗?


    小明处过三个对象 都是波霸类型的 但是她们的大小不一样
    分别是 E F G cup

  18. 老赵
    admin
    链接

    老赵 2009-11-30 16:27:00

    @韦恩卑鄙 alias:v-zhewg
    过了,C就不错。

  19. 韦恩卑鄙 alias:v-zhewg
    *.*.*.*
    链接

    韦恩卑鄙 alias:v-zhewg 2009-11-30 16:34:00

    -0-

     I波霸 女= C波女 as I波霸;  //这一句会得到一个空引用 为什么呢?

    我们检查代码

    发现如下声明

    public class C波女:I女,I能用
    {
    }
    
    
    public class D波女:I女,I波霸
    {
    }
    
    



    恍然大悟!原来 C波女并没有实现I波霸接口 而实现了I能用接口!


  20. 张蒙蒙
    *.*.*.*
    链接

    张蒙蒙 2009-11-30 17:17:00

    小思考大问题。
    支持老赵思考问题精神。

  21. 幸存者
    *.*.*.*
    链接

    幸存者 2009-11-30 18:08:00

    @RednaxelaFX
    我怎么记得CLR会在一定条件下回收native code,等到下次调用时会重新JIT。

  22. winter-cn
    *.*.*.*
    链接

    winter-cn 2009-11-30 18:19:00

    Jeffrey Zhao:
    @winter-cn
    那个,到底啥叫水呀……


    对一个博客的看客而言 不好玩就是水 呵呵

    不过大家对于水的态度是不同的 我觉得呢 自己的博客想写啥写啥 旁边围观的人看个开心就好了

    1楼可能觉得你这文章有点简单了吧

  23. 老赵
    admin
    链接

    老赵 2009-11-30 18:22:00

    @winter-cn
    我觉得吧……这两篇东西除了简单之外,没啥用倒是更大的问题……

  24. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-11-30 18:32:00

    幸存者:
    @RednaxelaFX
    我怎么记得CLR会在一定条件下回收native code,等到下次调用时会重新JIT。


    当前版本里,桌面的不会,Compact Framework里的会。

  25. 老赵
    admin
    链接

    老赵 2009-11-30 18:34:00

    @RednaxelaFX
    这么做的好处是什么呢?节省内存吗?

  26. 幸存者
    *.*.*.*
    链接

    幸存者 2009-11-30 18:35:00

    @Jeffrey Zhao
    有没有用这个问题说得大点是个哲学问题。能在工作中用得上与能引起人的思考哪种更有用?艺术有用么?有的国家连军队都没有却不能没有艺术。

  27. winter-cn
    *.*.*.*
    链接

    winter-cn 2009-11-30 19:01:00

    Jeffrey Zhao:
    @winter-cn
    我觉得吧……这两篇东西除了简单之外,没啥用倒是更大的问题……


    边边角角可有可无的小知识 呵呵
    今天写了段C# 忽然觉得这tmd才是生活啊! 没异常的C++简直是虐人

  28. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-11-30 19:20:00

    Jeffrey Zhao:
    @RednaxelaFX
    这么做的好处是什么呢?节省内存吗?


    code pitch可以经常保持在堆上的native code的“整洁”——只有还在被用到的native code才继续留在堆上,已经不用的代码抛弃掉。省内存是很重要的一面,对Compact Framework来说这是实现code pitch的主要驱动因素;CF会在收到低内存通知时自动做code pitch。
    如果一个JIT如果很动态,则为了提高代码速度可能会先做些乐观的假设,以此为基础做诸如内联虚方法、不显式检查空指针之类的的激进优化;一旦在代码执行过程中发现原本的乐观假设不成立,就有可能要重新编译,老代码自然需要抛弃。这个时候code pitch则用于支持激进优化。HotSpot就充分利用了这点。
    当然,保持一个比较紧凑的代码堆对locality也是有好处的。

    CLR的JIT相当静态,使用code pitch无法用于支撑激进优化(因为CLR的JIT不做激进优化);或许现在内存确实宽松,或许CLR生成的native code在多数案例里都占不了多少空间,反正当前版本的CLR是不通过code pitch来省内存的。Compact Framework在硬件资源较差的环境下工作,这才实现了code pitch。

    (注:我提到“CLR”的时候特指微软实现的运行在PC上的CLR。Compact Framework的运行时到底叫什么我不太清楚。Micro Framework里的一般是叫做TinyCLR。Mono的话我会就叫Mono。只有当某个描述是针对spec的,对上述所有实现都成立时,我会说“CLI”)

  29. 幸存者
    *.*.*.*
    链接

    幸存者 2009-11-30 20:34:00

    @RednaxelaFX
    我记得sun jdk的早期版本对bytecode是完全解释执行的,似乎即使现在的hotspot也只对常用的代码进行jit编译。
    其实我一直觉得不考虑性能因素的话,解释执行比jit在可移植性上有着天然的优势,非常适合Compact Framework。可惜看你这么说来Compact Framework并不支持解释执行。

  30. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-11-30 21:07:00

    幸存者:
    @RednaxelaFX
    我记得sun jdk的早期版本对bytecode是完全解释执行的,似乎即使现在的hotspot也只对常用的代码进行jit编译。
    其实我一直觉得不考虑性能因素的话,解释执行比jit在可移植性上有着天然的优势,非常适合Compact Framework。可惜看你这么说来Compact Framework并不支持解释执行。


    嗯,你说得都没错。Sun最早期的JVM确实是纯解释的,然后自己开发了带JIT的版本,在收购了Longview之后没多久推出了基于Strongtalk的HotSpot,并用HotSpot替代原本Sun自己写的JIT编译器成为Sun的默认JVM。
    前面我在提到激进优化时也说了,“可能重新编译”。HotSpot并不总是将乐观假设不成立的方法立即重新编译;在判断重新编译的收益低时会直接退回到解释模式。

    当前的.NET Compact Framework也是纯JIT的,确实没解释模式。
    CLI里有个神奇的东西,叫OptIL。我一直怀疑这个东西是不是原先为可能实现的解释模式而准备的。可惜相关资料太少,而OptIL当前没有任何主流CLI实现了。DotGNU的Portable .NET倒是有解释器实现,但没用OptIL。

    可移植性这种事情也是要看你的解释器是如何实现的。HotSpot里虽然有C++实现的版本的解释器,但现在默认的解释器还是历史更老的、用汇编写的解释器,叫做template interpreter。它会在VM启动的时候将实现解释器功能的函数从汇编模板生成为实际native code。(注意:这跟动态编译的部分没有关系)。使用汇编的好处是可以做很多很底层的操作,例如直接修改栈指针、精确生成跳转等。启动时才生产代码的好处是VM掌握了生成的代码的所有信息,包括地址和大小,方便实现硬链接,减少跳转的间接层数。这种解释器对平台依赖性极高,可移植性相当差。这就是高性能解释器的真相……
    有人为了让HotSpot能更方便的移植到别的平台上,开始了用纯C++实现的“Zero”解释器和“Shark” JIT项目;目前已经合并到OpenJDK中继续开发。“Zero”就是以“零汇编”为目标的,但它的速度跟HotSpot默认的template interpreter还是有差距的。Shark则是基于LLVM的JIT,靠LLVM的不同后端来支持移植。

    有些用C实现的解释器会使用利用“computed goto”来实现threaded-code interpreter,需要用到Sun的CC或者GCC的扩展,而且很难精确控制C编译器生成的代码,写起来就很麻烦。

    直观的解释器实现当然容易移植;稍微复杂一些的,如果只使用了标准C来实现的话移植性也会不错。只要涉及native code,可移植性就会下降得非常快。但有些提高性能的trick还是靠汇编实现起来方便(叹气

  31. 老赵
    admin
    链接

    老赵 2009-11-30 21:55:00

    你们说的东西要么我本来就知道,不知道的我也看得懂,太自豪了……
    跟着RFX真的可以学好多呀……

  32. 韦恩卑鄙 alias:v-zhewg
    *.*.*.*
    链接

    韦恩卑鄙 alias:v-zhewg 2009-11-30 22:34:00

    Jeffrey Zhao:
    你们说的东西要么我本来就知道,不知道的我也看得懂,太自豪了……
    跟着RFX真的可以学好多呀……


    跟着自豪

  33. 小城故事
    *.*.*.*
    链接

    小城故事 2009-11-30 22:48:00

    看到老赵在优酷上传的视频了,赞一个。不过听力还是差点,能否加点说明,至少可以明白在讲什么。

  34. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-12-01 02:21:00

    Hmm...发现在1.0 RTM之前的Compact Framework是用解释方式实现的,到1.0 RTM的时候才换成了用JIT。

  35. 徐少侠
    *.*.*.*
    链接

    徐少侠 2009-12-01 07:42:00

    以我的经验

    回帖多的一定是有意义的文章
    呵呵

  36. oec2003
    *.*.*.*
    链接

    oec2003 2009-12-01 09:05:00

    徐少侠:
    以我的经验

    回帖多的一定是有意义的文章
    呵呵


    这个还要看回帖的质量
    不过老赵这里回帖的质量都还挺高

  37. 老赵
    admin
    链接

    老赵 2009-12-01 09:15:00

    @oec2003
    关键还是看人,其实我这里就靠RFX,脑袋,Ivony等老大们撑着,如果他们把评论都写成独立文章,我这里就安静了,嘿嘿。

  38. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-12-01 10:53:00

    我是跟着脑袋和RFX混回复的小弟,众人可无视之。。。。。

  39. 装配脑袋
    *.*.*.*
    链接

    装配脑袋 2009-12-01 11:57:00

    GC.KeepAlive()通常应该是你方法最后一句。虽然这样看上去有些别扭~

  40. 老赵
    admin
    链接

    老赵 2009-12-01 12:01:00

    @装配脑袋
    那其实感觉效果就和自己写的普通的一个NoInline空方法效果差不多嘛。

  41. 装配脑袋
    *.*.*.*
    链接

    装配脑袋 2009-12-01 12:04:00

    @Jeffrey Zhao
    是啊,不过习惯就好了~ 这相当于就是提供了一个不会被任何诡异的优化所影响的那种空方法。。

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我