Windows Mobile 进阶系列.第二回.初窥.NET CF类型加载器
第零回.序和属性
第一回.真的了解.NET CF吗?
第二回.初窥CF类型加载器
摘要
对可执行的应用程序,它的生命是从Load开始的,一个.NET 的程序,某种程度上可以说它的生命是从加载类型开始的。本文阐述了在.NET CF中的Type Loader的工作原理,并结合示例说明了如何让您的应用程序启动更快。
Keywords
.NET Compact Framework,Type Loader, JIT ,Generic,Dictionary
1. 设备不能承受之慢
等待是很痛苦的,让用户等待是不人道的。现在PC机上的程序也许感觉不是很明显,因为桌面计算机性能普遍较佳,而且一般的应用程序不会涉及海量数据的运算,日常的程序即使有性能上的某些缺陷,用户也不会明显的察觉到。
然而在CPU处理能力和内存均有限的移动设备上,计算机的工作能力就没有那么可观了。也许一个简单的程序就能让你的设备陷入肉眼就能识别的性能危机。设想一下用户怀着愉悦的心情在你的程序中选择了一个菜单项,但是他那台不怎么样的设备却需要反应数十秒,用户也只能望着屏幕中央不断旋转的光标兴叹了,这无疑对用户来说是一个打击,软件开发人员更是颜面无光。
好吧,下面我们就来看看下面一个简单的程序是如何折腾你的CLR的,虽然我刻意将它极端化了一点点J
{
public Form1()
{
InitializeComponent();
int[] r = new int[6];
int n = 0;
安置一些计时的环节,并调用funcX()触发Type Loader
//将结果保存到本地文本文件中
using (StreamWriter writer = new StreamWriter(@"\Temp\LoaderPerf.txt", false))
{
for (int i = 0; i < n; ++i) writer.WriteLine(r[i]);
}
}
/// <summary>
/// 返回一个整型值,单位为毫秒
/// 指示从Type Load开始到方法开始执行的时间差
/// </summary>
/// <returns>初始化类型所耗费的时间(毫秒)</returns>
public static int func0(int start)
{
int diff = Environment.TickCount - start;
Maps0.func();
return diff;
}
与func0一致的其他五个方法
}
/// <summary>
/// 定义了一个有些夸张的类
/// 它定义了5个枚举5个泛型字典并做了初始化
/// </summary>
public static class Maps0
{
public enum Enum1 { i1, }
public enum Enum2 { i1, }
public enum Enum3 { i1, }
public enum Enum4 { i1, }
public enum Enum5 { i1, }
private static readonly Dictionary<Enum1, int> map1 = new Dictionary<Enum1, int>();
private static readonly Dictionary<Enum2, int> map2 = new Dictionary<Enum2, int>();
private static readonly Dictionary<Enum3, int> map3 = new Dictionary<Enum3, int>();
private static readonly Dictionary<Enum4, int> map4 = new Dictionary<Enum4, int>();
private static readonly Dictionary<Enum5, int> map5 = new Dictionary<Enum5, int>();
public static void func() { }
}
与Maps0一样的其他五个类
以上代码在Windows Mobile 6 Professional的模拟器上运行时,得到的txt文件如下:
可以看到反应(加载类型的时间)基本在1秒左右,而且逐渐增长,原因下文中会做详细的解释,当然,这些数据和机器性能也是有关的。但是差别不会太大。
同样的程序在我的Samsung i718上测试结果为如下:
406
377
553
993
1678
2126.
虽然跟模拟器相比开始时效果要好一点,但还是一个数量级的
如果在你的应用程序加载几个类型就要花费CLR以秒为单位的时间,那用户估计要抓狂了。
2. 到底发生了什么?(What happened when JITing?)
前面只是举了一个略有夸张的例子,也许你还不明白它夸张在哪里,但它确实是有可能存在的,比如说我们的查询服务,比如通信数据中要对某些对象进行序列化处理,比如多机通信系统有时可能要用到的很多的Hashtable,Dictionary等等。下面我们就来看看,刚才那段代码在执行的时候,CF CLR发生了什么。
在代码执行到Maps0.func()的时候,对方法func()的调用会触发CF CLR的Type Loader的工作(这是JIT的工作方式,我也曾在这里提到)。为什么会在这里触发Type Loader呢?
因为之前的代码并没有访问到Maps0咯,如果Type Loader检测到该类型已经被构造出来,那么它将立即return,以免做重复的工作。
加载Maps0又会立即导致这个包含Maps0的模块(module)被标记为忙碌(busy)。
注意,这里说的是module而不是说这个App.exe,尽管此时的app的确是busying。
在加载Maps0的过程中,CF Loader会遇到Maps0所包含的某些静态类型字段如Dictionary< Maps0.Enum1, int>,这些类型同样是他从来没遇到的,于是,在JIT的时候这样的Load会递归的进行下去,直到涉及的类型都被Load完毕。
好,说到加载Dictionary< Maps0.Enum1, int>,这时,由于mscorlib.dll包含有Dictionary<T, U>的定义,现在mscorelib.dll同样会被打上一个忙碌中(busy)的标记。
接着,CF Loader会加载DictionaryEntry<Maps0.Enum1,int>,因为在Dictionary的内部会有一些数组结构,用来存储键值对,而每一个这样的键值对是一个DictionaryEntry,关于DictionaryEntry你可以在MSDN找到更详尽的解释。
而在加载DictionaryEntry<Maps0.Enum1,int>的时候,CLR又发现了类型Maps0.Enum1(一开始在Dictionary的时候就遇到了Maps0.Enum1),这时Loader将再次尝试去加载Maps0.Enum1。然而当Loader抛开mscorlib.dll(因为DictionaryEntry并不在mscorlib.dll中)而转向程序集app.exe它会发现,app.exe中那个包含Maps0的模块被标记为busy,所以它并不能访问,这其实是一个博弈的问题。所以上面的代码初看似乎除了冗余似乎没什么大问题,但实际上并不是一种好的实现方式。
而此时访问的Maps0.Enum1将被描述为处于一个“部分加载”(待结束) 的状态。
同样,包含对Maps0.Enum1引用的DictionaryEntry<Maps0.Enum1, int> 和Dictionary<Maps0.Enum1, int> 也会处于这样的部分加载的状态。由于Loader从对这些类型的加载中不成功地返回,最后它会扫描当前的module中的每一种类型,不管他们是返回的是完整加载还是“部分加载”状态。不过个人认为这里面有一定的可以优化的空间,虽然这样的扫描是必要的,因为不能保证返回的加载成功的类型运行时不会访问到“部分加载”的类型。
所有的加载到的类型扫描完毕后,Loader此时会尝试去完成扫描到的所有“部分加载”的类型。最终Loader 还是会回到起初对Maps0的调用的地方,并发现Maps0.Enum1处于“部分加载”的状态,这个时候才将Maps0的Enum1完整加载。
接下来,Loader会按照相同的方式去加载Maps0的其他成员。被Enum1“阻碍”的其他类型也都将被顺利加载,因为此时的Loader被告知Maps0.Enum类型已经加载过了。上面提到的这个问题就像一条拉链,如果每一个环节都拉紧了,最终还是只能从头把它拉开。
可是还没完呢,别放松得太早!这才一个Enum1呢,接着执行下去Loader会尝试加载Dictionary<Maps0.Enum2, int>,而这时候同样的问题会再次发生,因为在加载DictionaryEntry<Maps0.Enum2, int>的时候,Maps0.Enmu2之前已处于“部分加载状态”,然后以上的一切会重演。如此循环下去,到足够的次数Maps0类型终于被Load完毕了!汗,确实是非常纠结和缓慢的过程,每一次,当Type Loader尝试在App.exe中加载EnumX并以失败返回时,它都必须循环访问在mscorelib.dll和App.exe中每一个已加载的类型 (包括未加载完全的类型)。
现在你也许会感叹了,上面那段看似和谐的代码给你带来了多么痛苦的一次CLR之旅!Got twisted?!头晕了吗?
另外,我们可以从输出的结果中看到,从Maps0到MapsN,所耗费的时间是越来越长的,这又是什么原因呢?其实仔细分析我们可以知道,由于加载的类型逐渐增多,当每一次遍历他们,并获取其加载状况的时候,所需要的代价(资源,时间)也会越来越多,这个是很好理解的。
最后,再来回顾一下上面描述的问题,可以发现,主要的耗费在于那个隐藏的博弈的怪圈,简单来说,事情就是这样的: 类型 A在模块A.dll中,但它引用了一个类型B,但是这个类型B又引用了另外一个类型A2,有趣的是这个A2却正好位于已谢绝访问的A模块中,而且A2并没有被完全加载。问题就在这里,所以必须让A2先加载完毕,但是这之前Loader却要扫描所有已加载的类型,现在应该很清楚了吧。头还晕的同志可以喝杯茶再多看两遍。呵呵。虽然这里的泛型字典有点特殊,但是它很好的反映了问题,接下来,来看看改如何优化这个程序。
3. 较好的解决方案
针对这个问题,我们该如何提高程序的性能呢?
这里有两种方式:
1)我们可以通过转移的方式打破这种循环依赖的结构。把Maps类型或者Enum类型放到另外的程序集中去。
2)我们可以通过某些手段让这所有的Enum类型都先加载,后面的Maps的内容涉及EnumX的就不再需要重复加载了。
下面来来看看这两种方案具体是如何实现的
第一种方案是从编程原则上面说的,这很好理解,我们在编写程序的时候应当使所有涉及的引用是“前向”的,也就是说,尽量避免这种往“回”的引用出现,以免造成环形引用。这有点像那个Philosopher的例子,只不过那个例子会造成Deadlock,而这里的lock最终会由CLR费力的解开。而这都是应该尽量避免的。
第二种方案也比较好理解,但是实现起来可能会比较繁琐,
也许你会设想,要是在调用Maps0.fun()之前先new一个Enum1的实例不就好了吗?但这是不可能实现的,成员类的调用还是会首先导致父类的加载。而这还是回到了之前出现的那个问题上了。
我们需要作的应该是把EnmuX类型均移到Maps0类型之外,并放到另一个类型中去,比如class EnumsInMaps0,或者就让他们在最外层的class内也行。然后去让这个EnumsInmaps中的Enums先实例化,如下:
//我把Maps0,Maps1,Maps2中的Enum那出来,放到EnumInMaps中:
{
public enum Maps0Enum1 { i1, }
public enum Maps0Enum2 { i1, }
public enum Maps0Enum3 { i1, }
public enum Maps0Enum4 { i1, }
public enum Maps0Enum5 { i1, }
public enum Maps1Enum1 { i1, }
public enum Maps1Enum2 { i1, }
public enum Maps1Enum3 { i1, }
public enum Maps1Enum4 { i1, }
public enum Maps1Enum5 { i1, }
public enum Maps2Enum1 { i1, }
public enum Maps2Enum2 { i1, }
public enum Maps2Enum3 { i1, }
public enum Maps2Enum4 { i1, }
public enum Maps2Enum5 { i1, }
public void LoadMessage()
{
// MessageBox.Show("Enums Loaded!");
}
}
注意,这里也许单纯的new还不行,即使是你为这个EnumsInMaps创建了一个实例,仍然不能保证它的每个成员都被JIT了。我们姑且踏实一点,这里我不妨这样:
{
EnumInMaps.Maps0Enum1.i1.ToString();
EnumInMaps.Maps0Enum2.i1.ToString();
EnumInMaps.Maps0Enum3.i1.ToString();
EnumInMaps.Maps0Enum4.i1.ToString();
EnumInMaps.Maps0Enum5.i1.ToString();
EnumInMaps.Maps1Enum1.i1.ToString();
EnumInMaps.Maps1Enum2.i1.ToString();
EnumInMaps.Maps1Enum3.i1.ToString();
EnumInMaps.Maps1Enum4.i1.ToString();
EnumInMaps.Maps1Enum5.i1.ToString();
EnumInMaps.Maps2Enum1.i1.ToString();
EnumInMaps.Maps2Enum2.i1.ToString();
EnumInMaps.Maps2Enum3.i1.ToString();
EnumInMaps.Maps2Enum4.i1.ToString();
EnumInMaps.Maps2Enum5.i1.ToString();
}
直接在Enums上调用方法,这总可以了吧,好,现在我们就可以让enumX顺利加载了,也可以随意实例化了,不用担心什么循环依赖。你也不必总是保留一个实例,类型一旦被加载,在这个应用程序的生命周期内它都会被标记为已加载,而且生成的本地代码也会被缓存(如果没有内存问题的话)。所以即便没有了实例,这个类依然存在着。
现在,我们需要把对这些enumX的处理交给一个函数去做。当然,这个函数应当是较早调用的,至少要比加载Maps或者他的成员而调用它们的时候更早些。
注意:为了便于在同一份代码中对比,这里只对Maps0,Maps1,Maps2进行了修改,后面三个类依旧保持原样
执行结果如下:
在我的Samsung i718上面测试结果如下:
4
4
42
845
1358
1797
可以看到,经过修改的Maps0,Maps1,Maps2的Load时间加起来不过几十毫秒,较之前面的代码,速度提高了百倍左右,看来我们修改的效果是明显的。
4. 类型加载的几句你必须知道的废话
再多说几句关于类型构造器的话吧。
首先,编写类型时,要时刻想到CLR在加载它的时候会有哪些行为,你的代码逻辑是否会导致交叉引用而对CLR造成前面提到的困惑。
另外,构造时要注意CLR的“自动化”行为。要弄清需要的是静态构造器还是实例构造器,默认的CLR是否会调用无参构造器,是否每个构造器都导致了其他成员的初始化等等。这里面仍然有着可优化空间。
最后,关于前两天有网友问到的,Windows Mobile上的程序是否有必要检查其版本唯一性?也就是说,是否需要自己在程序中保证当前运行的只有一份自己的程序的实例。
这个问题要分情况考虑:
如果你的程序是纯本地代码编写(without CLR),那么跟在WinCE下无异,你需要在你的程序检查当前的运行的程序(当然,方法很多,比如CreateEvent捕获异常等等,这里不做详细介绍)。
如果你的程序是托管的(with CLR Supports),那么你设备上的.NET Compact Framework CLR会帮你做这个维护,保证你的应用程序不会出现多份。CLR此时的工作如下模式:
首先,CLR会找到你的程序入口点,在尝试加载你的应用程序之前它会检查程序集的信息,看要加载的应用程序是否在当前已请求Singleton的程序清单上,如果没有则证明是首次执行程序,然后再加载该应用程序到CLR中,然后请求Singleton保护。
简单说来如下所示:
AppStart();
CheckSingletonMutex();
LoadAppIntoCLR();
AcquireSingletonMutex();
但是,当你启动的间隔极短,在Check Singleton还没完成的时候,还是有可能出现多个你的应用程序实例同时存在的情况。所以说,作为正式的产品,这样的检查还是有必要的。
点此处下载代码示例
总结
程序的性能总是在我们不经意间浪费掉了。PC机的开发也许感觉还不是太明显,作为移动设备,性能问题却十分要命。
类型的加载是JIT的,托管应用程序是CLR掌管的一个运行实例,JIT是CLR的必杀技,CLR是.NET的灵魂。作为移动设备的程序员,在享受CF CLR带来的种种便利的同时,也应该为CLR想想,尽量减轻它额外的负担,让你的应用程序享受裸奔一样的快感!
Regards
Reference:
MSDN
Jeffrey Richter CLR via C# Second Edition
.NET Compact Framework 社区
---
©Freesc Huang
黄季冬<fox23>@HUST
2008/3/1
posted on 2008-03-01 01:23 J.D Huang 阅读(3478) 评论(21) 编辑 收藏 举报