Windows Mobile 进阶系列.第二回.初窥.NET CF类型加载器

相关文章
第零回.序和属性
第一回.真的了解.NET CF吗?

 

第二回.初窥CF类型加载器

摘要
对可执行的应用程序,它的生命是从Load开始的,一个.NET 的程序,某种程度上可以说它的生命是从加载类型开始的。本文阐述了在.NET CF中的Type Loader的工作原理,并结合示例说明了如何让您的应用程序启动更快。
Keywords
.NET Compact FrameworkType Loader JIT GenericDictionary

1.       设备不能承受之慢

等待是很痛苦的,让用户等待是不人道的。现在PC机上的程序也许感觉不是很明显,因为桌面计算机性能普遍较佳,而且一般的应用程序不会涉及海量数据的运算,日常的程序即使有性能上的某些缺陷,用户也不会明显的察觉到。

然而在CPU处理能力和内存均有限的移动设备上,计算机的工作能力就没有那么可观了。也许一个简单的程序就能让你的设备陷入肉眼就能识别的性能危机。设想一下用户怀着愉悦的心情在你的程序中选择了一个菜单项,但是他那台不怎么样的设备却需要反应数十秒,用户也只能望着屏幕中央不断旋转的光标兴叹了,这无疑对用户来说是一个打击,软件开发人员更是颜面无光。

好吧,下面我们就来看看下面一个简单的程序是如何折腾你的CLR的,虽然我刻意将它极端化了一点点J

   public partial class Form1 : Form
    
{
        
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?)

前面只是举了一个略有夸张的例子,也许你还不明白它夸张在哪里,但它确实是有可能存在的,比如说我们的查询服务,比如通信数据中要对某些对象进行序列化处理,比如多机通信系统有时可能要用到的很多的HashtableDictionary等等。下面我们就来看看,刚才那段代码在执行的时候,CF CLR发生了什么。

 在代码执行到Maps0.func()的时候,对方法func()的调用会触发CF CLRType 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处于“部分加载”的状态,这个时候才将Maps0Enum1完整加载。

接下来,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.dllApp.exe中每一个已加载的类型 (包括未加载完全的类型)

现在你也许会感叹了,上面那段看似和谐的代码给你带来了多么痛苦的一次CLR之旅!Got twisted?!头晕了吗?

另外,我们可以从输出的结果中看到,从Maps0MapsN,所耗费的时间是越来越长的,这又是什么原因呢?其实仔细分析我们可以知道,由于加载的类型逐渐增多,当每一次遍历他们,并获取其加载状况的时候,所需要的代价(资源,时间)也会越来越多,这个是很好理解的。

最后,再来回顾一下上面描述的问题,可以发现,主要的耗费在于那个隐藏的博弈的怪圈,简单来说,事情就是这样的: 类型 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中:

   class 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了。我们姑且踏实一点,这里我不妨这样:

         public static void PreLoadEnums()
        
{
            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,Maps2Load时间加起来不过几十毫秒,较之前面的代码,速度提高了百倍左右,看来我们修改的效果是明显的。

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掌管的一个运行实例,JITCLR的必杀技,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  阅读(3468)  评论(21编辑  收藏  举报