SUMTEC -- There's a thing in my bloglet.

But it's not only one. It's many. It's the same as other things but it exactly likes nothing else...

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::
博主您好! 
首页是发表精品文章的地方。 
您的博文“QUIZ:一个有8个属性的匿名类大约会占多大的文件大小?”被移出首页,由此给您带来的麻烦,请谅解! 
首页文章要求:原创,排版整齐,文中有文字明确说明文章的主题,内容对程序员有帮助。 
下列类型的文章不允许发到首页: 

1) 转载;2) 只有代码;3) 简单的提问;4) 软件发布;5)人才招聘;6) 包含推广或广告内容;7)活动信息;8)关闭评论功能的随笔;9)不完整的内容。

 

既然抛砖引玉中的抛砖不能出现在首页,那我也不想引玉了。真遗憾,在博客园中用了这么长的时间,我终于有了当年从CSDN退出时的感觉:实在无法理喻这里的风气了。难道C#自己写的一个自定义分页控件就是一个很有价值的文章?我的意思并非这样的文章就不好,而是说,一个引起思考的短文居然和这个比都没有价值,我不知道这样的地方价值在哪里?或者说,这样的地方已经没有多少营养了,需要认真考虑另起炉灶了。

 

我想问一下,认为不值得放首页的那些个同学,你们有几个能回答出我说的这个问题?你们有几个自己真的做过这方面的研究?知道MetaData都包含什么?这个是一个简单的提问?

 

前阵子微博上还有人调侃,说:

 

其实我倒不是觉得不能写一些非技术的东西,或者制造一些话题。但是,现在真不是观众说了算,一些我觉得没啥看头的东西占据了首页,一些其实蛮有看头的东西却被大量水文瞬间冲掉。好,废话不多说,最后给出这个被强制移出首页的QUIZ的答案。

 

一个包含了8个属性的匿名类,会占用大约5K的文件存储空间。也就是说,如果你使用了10个这样的匿名类,你的文件大小就会导致你的文件增大大约50KB。对于一个桌面应用来说,50KB算不上什么,但对于一个SilverLight应用来说,这就不是一个小数目了。更可怕的是,如果我们不知道这个问题,使用了一个比如说42个属性的匿名类,就会导致你的文件增长大约46KB的大小。当然,这里有严重的水分,原因后面会简单提到。


在开始进行一些简单的分析之前,也许你需要自己去了解一些有关CLI的知识,比如什么是BlobHeap、UserStringHeap、StringHeap、TableHeap等,以及里面是用什么格式进行组装的。原本呢,我觉得这些大家只要用Google搜索一下CLI和MetaData就会出来了,可是我现在觉得,以博客园的平均水平而言,也许连这个能力都不具备。那么好吧,下面这个是链接,有能力的请自行阅读:

http://download.microsoft.com/download/d/c/1/dc1b219f-3b11-4a05-9da3-2d0f98b20917/partition%20ii%20metadata.doc

 

下面,我们来对这一个问题进行一下简单的剖析。为了让问题更明显和突出,我们对一个有42个属性的类进行分析。

 

首先,整个匿名类哪些部分会占用的比较多? 根据统计,在TableHeap中使用了5k,StringHeap中使用了7k,Blob使用了27k,UserString使用了1k,Body占用了近5k。需要说明的是,除了Body部分(IL代码)相对比较准确之外,其它的部分的统计是不准确的。这是因为根据规范,相同的内容很可能会自行排重而只记录一遍。而同一个内容在整个程序集中会多次被使用到,据一个例子:比如属性的访问器名称get_PageId,可能在多个类当中都有该属性;此外还有其它很多原因可能会导致重复统计。根据我的估算,可能需要打个3折,即便如此也得占用大约14k的空间。为了便于讨论,我们这里就先忽略这些重复的统计。


途中的Blog占用空间非常大,至于为什么,这是我尚未解开的部分。也许是因为:

1、BlobHeap包含了太多的东西,比如一个函数的签名,标签(Attribute)所使用的具体参数等;

2、使用的场景也比较多,比如TableHeap中的MethodRef、Method、Param等等,几乎各个Table都可能有指针指向Blob。甚至连MethodBody当中的某一句IL,比如call System.Linq.Quearyable.Where'1 ... 等,都可能会在Blob里面加一些东西;

3、从Reflector看到的情况无法解释这一部分异常增大的原因,甚至不排除这个工具本身哪里有Bug导致统计数据出现了错误。

但无论如何,也不是重复统计所能解释的,关于这部分后面会给出两个图进行说明。


抛开奇怪的Blob部分,还有一些很容易发现的问题。比如,为什么一个匿名类需要使用1k的UserStringHeap呢?UserStringHeap中记录的是你代码当中的字符串常量,比如说下面这么一段代码:

void Main()
{
  Console.WriteLine(
"Hello world!");

 

这么一个语句,当中的"Hello word!"就是要进入UserStringHeap的,大约会占用23个字节。可是我们的匿名类里面,怎么会有字符串常量呢?原来,编译器在生成匿名类的时候,为了便于你调试,会在类的前面打上一个DebuggerDisplayAttribute标签,比如:(为避免泄露些什么,字段名称已经修改,字符串中的...表示后面还有好长好长……)

 

DebuggerDisplay(@"\{ PageId = {PageId}, ReadId = {ReadId}, RefId = {RefId}, Guid = {Guid}, TypeId = {TypeId}, CategoryId = {CategoryId}, Title = {Title}, Mode = {Mode}, Setting = {Setting}, Tags = {Tags} ... }", Type="<Anonymous Type>")]

 

 

打上这一个标签的好处是,当你进行断点调试的时候,你可以看看这个匿名类里面的属性值都是什么。可正是这个标签,导致了UserString的占用。由于不同匿名类中,属性名称可能会不一样,就算一样,顺序也可能不一样,因此这串字符串也就不太可能完全相同。于是,你用的匿名类越多,这种无谓的占用就会越多。幸好,这个问题只会出现在Debug的编译结果中,对于Release发布则没有这个标签。

 

接下来,也许你会奇怪,对于一个42个属性的匿名类,所使用的StringHeap会达到7k,好吧,这是我的工具重复统计导致了过分的放大。但是,仔细看一下匿名类你就会发现:

1、一个有着N个属性的匿名类实际上是一个有N个泛型参数的泛型类。假设有一个属性是PageId,则:

2、属性名称叫做PageId;(7个字节,注:C字符串格式最后有一个字符0)

3、属性的访问器叫做get_PageId;(11个字节)

4、属性所对应的成员叫做<PageId>i__Field;(17个字节)

5、属性的泛型参数名称叫做<PageId>j__TPar;(16个字节)

6、匿名类名称为AnonymousType#`N,数字#表示第#个匿名类,数字N表示有N个属性。那么对于有着8个属性的匿名类1,长度就是17个字节,对于有着42个属性的匿名类2,长度就是18个字节;

7、假设我们属性名称的平均长度就正好是6个,那么42个属性的匿名类就至少占据了2k有多。

 

从上面这个部分,我们就可以发现,假如我们把几乎不会影响一般运行,甚至对反射也没有太大影响的成员名和泛型参数名优化一下,变成平均4个字节(甚至是0字节),那么也可以减少超过一半以上的空间占用。对于使用匿名类较多的某个dll来说,光是这部分可能就可以优化掉大约10k左右的大小。

 

另一个让人吃惊的地方,是MethodBody占用非常大,大5k之多,平均一个属性有大约100多个字节。要知道,一个如下的属性:

public int PageId
{
  
get
  {
     
return _pageId;
  }

对应的il也就如下三句:

ldarg.0
ldfld thisType._pageId;
ret

 

 共6个字节。如果是Debug编译,会多出额外3句以便于调试,也就在多4个字节而已。换而言之,有其它的函数在哪里捣鬼,捣鬼的那几个函数分别是:

Equals、GetHashCode、ToString以及构造函数.ctor。

 

 

如果我们用Reflector打开这个函数来看,Equals、GetHashCode、ToString以及构造函数都要访问到每一个属性。其中对于构造函数来说,这是几乎不可避免的,因为需要对匿名类的每一个属性进行赋值操作。但是为啥需要重写Equals等其他三个函数呢?这是因为这些对象可能会被用到Dictionary的Key中,此时就必须重写Equals、GetHashCode、ToString这三个函数,而这三个函数加起来就得占用大约4k的大小。准确说,所有匿名类的属性总数,决定了整个dll中该部分代码的大小。如果你这个dll中一共有10个匿名类,每个大约10个属性,那么光是代码部分,这三个函数就占用了大约10k的大小。不要忘了,除了代码之外,我们还需要因此写入一些元数据,以及为数不少的UserString。而实际上用到这三个函数的几率非常非常的小,完全可以通过实时动态Emit来完成,而不需要占用这么大的代码空间。当然了,进行动态生成的代码也会占用不少的空间,但如果你做的是一个很大的项目,比如你有很多的页面,每个页面用到不同的Dll,里面都有不少的匿名类,那么这点的优化成本很可能是值得的。而另一种方式也许更好,那就是假设该类不会被当作对比的Key来使用,而是当作一个普通的类,那你可以干脆裁减掉这三个方法(这种裁剪我还没有试验过,也许你需要自己进行尝试)。

 

最后,我们来看看Blob方面的一个奇怪的问题,那就是:匿名类的属性越多,则某一个属性需要用到的Blob就越大。比如说对于一个有42个属性的匿名类,其某个属性访问器所使用用到的Blob大小如下图所示:(包括方法签名,方法中调用某函数、使用某成员所产生的一个MemberRef记录所用到的方法签名等)

 

而一个只有8个属性的匿名类,其某个属性访问器所使用到的blob大小为:

 

目前我想到的合理解释是,由于需要返回该属性对应的字段,如下述IL代码:

ldfld !0 <>f_AnonymousType0'42<!<xxx>j_TPar, !<xxx>j_TPar, ...>::<PageId>i__Field 

这里需要产生一个针对该字段<PageId>i__Field的签名,而签名当中又带有当前类的各种泛型参数信息,因此造成了属性越多,占用Blob越厉害的结果。而至于这部分的Blob是否如我猜测,如是,是否会每个参数都要占用这么多还是其中一部分会被重复利用,都尚未可知。当然,也有可能我的程序有Bug造成的。关于此,我想我不会在博客园继续写了。

 

如果有兴趣的同学,可以去下载一个开源的项目,叫做Mono.Cecil,我的工具就是在这个项目的基础上去完成的。通过该项目的源代码,你可以很好的了解整个.NET文件的结构组成。当然,在你正式开始阅读这部分代码之前,最好先看看我前面提到的那篇文章,因为这个Mono.Cecil的项目里面,注释量基本是Zero。对于那些从来不喜欢Read the fuck code的同学,会是一种巨大的挑战。

 

最后,我宣布从博客园正式退役了,不玩了。有些东西抱怨太多就没意思了,每次发布都要头痛,到底是放首页呢还是放首页呢,还是放首页呢?然后要从一堆花花绿绿的各种我都不知从何下手的选项中,选一些我知道的不知道的东西,简直就是一场噩梦。

 

关键是,没错,我很不爽,你们把我觉得还比较有难度有挑战的小提问给挪出去了,你们不提倡思考了,开始喜欢浮躁喜欢造话题。那些相对来说没啥技术含量的东西,字数也不见得多出多少,不也照样放在首页吗?既然咱们的人生观价值观已然发生分歧了,那只好分道扬镳了。

 

我刚刚做了一个非常艰难的决定,那就是博客园,我不玩了。至于你们信不信,我反正是信了。走咯,回家了,拜拜了各位!

 

 

 

posted on 2011-08-19 19:25  Sumtec  阅读(5177)  评论(106编辑  收藏  举报