5理解.NETCompactFramework与性能优化本章内容开发常识理解精简版CLR引擎.NETCompactFramework性能统计表以编码方式检测性能性能指导尽管移动设备确实在硬件和操作系统方面获得飞跃性的改进,但它仍受原始的电力供应的制约,因此,对性能的考虑在开发过程中就显得尤为重要。同样,对依赖电池的设备来说,不仅出于对性能的考虑,而且还由于电池容量的问题,要尽量减少消耗。本章将讨论编写良好性能代码的各种原则。要取得良好的性能,关键是在开发过程中尽早确立性能目标,还要理解公共语言运行库(CLR)的内存管理,并避免在内存中产生不必要的“垃圾”。理解程序运行过程中运行库的工作情况与如何才能尽量减少运行库的开销同样重要。5.1开发常识在某些圈子里,开发者会把性能当作主要的驱动因素,应用能够想到的每一种优化措施,不顾一切地加速进程运行,这是一个误区。让代码快速运行没错,但目标应该是明确您的程序需要多快。例如,如果您的应用程序需要3s来执行某项操作(例如,连接到服务器,进行登录验证),且用户欣然接受,那么花时间来使这个操作更快并没有多大意义。在这种情况下,用户的接受程度决定了实现那种情况下所需要的性能标准。满足用户接受的性能标准才是程序的运行速度唯一的驱动力。如果需要更多因素来帮助您进行决策,那么,考虑下面几个问题:上一个版本运行速度有多快?竞争对手的产品运行速度多快?此外,决定是否继续优化代码的唯一评判标准,应该是用户是否接受这个速度。如果用户使用您的产品比之前用过的任何方法(包括手动操作)都方便,且速度与其他同类产品持平,那就没有必要再做优化。在撰写功能规格说明书时,应该指定最低的性能要求和理想的性能要求,同时要避免仅用“这个功能一定要快”这样的句子来描述对某个功能的速度要求。不幸的是,很多开发者在写了很多代码之后才发现他们的代码不够快,只好再尝试着去优化代码,希望能够在性能上有所改进。为了避免事后的麻烦,必须在开发进程前期指明对性能的要求,并列举客户的各种期望值。测试过程中要不断地验证那些功能是否满足规格说明书的要求,并要考虑代码更改是否会对结果产生负面影响。良好性能的程序代码不是事后设计出来的。例如,一个涉及在ListView加载选项的功能。实现这个功能后,发现加载10000条选项花费的时间长得难以接受。那么,您是希望试图对每个方法进行分析来进行优化,还是希望重新进行设计?在一个功能有限的设备上,无论您如何编写代码,加载10000条选项的速度都会十分糟糕。即使能容忍这种性能上的缺陷,要求用户在一次只能显示十几条选项的设备屏幕上,查找数以千计的选项,这样会有意义吗?当然,解决方案可能是,在默认情况下,每次仅加载一两百项,而当用户向下滚动时,再继续加载;或者,将这些数据按字母、类别或其他特性进行分类,并提供若干对前、后页面进行导航的按钮。最好是先将注意力放在可感知的性能上(例如,当用户单击按钮后,显示等待光标的那段时间),而不是一开始就过多地注意实际需要占用的开销上(例如,窗体的加载需要的时间)。要合理设计您的程序,使用户会不断地得到他们操作的反馈。如果两个程序完成一项任务时速度相同,而其中的一个由于提供了可视化的反馈信息,而感觉速度快了些,对此用户是会感到欣慰的。例如,您可以在屏幕上设置一个ProgressBar,显示忙碌光标,或在状态栏更新进度消息,以便提示用户工作的进展情况——不要指望用户会盯着一个空白的屏幕直到操作的完成。进度反馈很重要,而且,如果不事先做出设计,则是不容易实现的。例如,在用户单击搜索按钮10s后还不能在屏幕上显示结果,那可能不会被接受。然而,若每两秒在列表中加载一些即时搜索结果,即使整个搜索耗时15s,用户也会感到愉悦的。至此,我们已经给出了可以应用在任何软件开发项目中的通用性建议。而我们要提供的其他建议是,必须了解所面向平台的特性,对Microsoft.NETCompactFramework来说更是如此。在理解了.NETCompactFramework的公共语言运行库(CLR)后,您所熟知的每一项优化技术几乎都会派上用场。5.2理解精简版CLR引擎在软件开发中,开发者通常要平衡代码的时间复杂度与空间复杂度。这在设计时很容倾向于一边——有时速度快了,但消耗了很多内存空间(如,在内存中缓冲结果);或者有时,虽然缩减了内存占用,但处理速度又慢了(例如,通过延长每次请求的CPU处理周期来计算某一结果)。在精简版CLR环境中,要用空间(RAM)换时间(速度)不会总是奏效。您必须使编写的代码既能高效使用内存,又能快速运行。要明白其缘由,有必要先理解垃圾回收器(GarbageCollector,GC)和实时编译器(也称为“JIT编译器”或JITter,其中JIT代表just-in-time)的内部工作机理。下面并没有对CLR做全面讲解,而只是站在一个较高层面上,对影响性能的部分进行描述。我们的目标是尽量降低底层细节带来的不必要的复杂性。5.2.1JIT编译器编译MicrosoftVisualBasic或者C#代码时,所生成的二进制文件(即.exe或.dll文件)中不包含任何本地CPU(CentralProcessingUnit,中央处理单元)指令,而是“中间语言(IntermediateLanguage,IL)”代码。在运行时,JIT编译器会将每个IL方法进一步编译成本地代码,然后再执行。注意,JIT对某个方法的编译只发生那个方法被请求执行时。当某个方法被调用,它将检查是否有此方法的本地代码。如果有,本地代码将会被执行;若没有任何对应的本地代码,JIT编译器会把IL代码编译为本地代码,此本地代码会与其入口(存储在内存的缓冲区中)相衔接,然后,执行的无疑是这些本地代码。JIT编译器每次对方法的编译都将以牺牲性能为代价。要减少这种开销,就要试图降低深度方法的调用层次,避免过长的方法路径或递归,因为JIT编译器对短的代码路径(codepath)表现更为出色。在桌面的CLR中,这种开销仅发生一次,因为,所生成的本地代码将与其方法关联,直至应用程序退出。这表面上与精简版JIT编译器有所差异:由精简版JIT编译器生成的本地代码可能在运行时被丢弃(如,当系统发现内存严重不足时),这称作“代码丢弃(codepitching)”。若发生丢弃,很明显需要JIT对每个方法进行多次编译,从而影响性能。要了解更多关于“代码丢弃”的信息,请阅读本章后面5.2.2节。精简CLR与桌面版的另一点不同,是没有对本地映像的支持。换句话说,您不能够使用来自其软件开发包(SDK)中的Ngen工具在安装时生成本地映像(nativeimage),那意味着JIT还会在运行时的性能上受到制约。因为,本地映像的执行速度,大约比托管程序集快3~4倍。.NETCompactFramework2.0类库仅有5MB大小,已足以见得形势的严峻性了。1.内联JIT编译器有一个优化功能叫“方法内联”(methodinlining)。这表明一些方法可以与主调方法(callingmethod)进行内联。包含内联方法后,主调方法将膨胀,这就避免了对方法的调用。所有这一切发生在IL被JIT编译之后的机器代码层。若在托管代码层描绘内联的效果,看起来可能会像下面这两个方法:publicintCallingMethod(){//codethatperformssometaskAthis.SomeOtherMethod();//codethatperformssometaskC}privatevoidSomeOtherMethod(){//codethatperformssometaskB}在运行时,它们将变成一个单独的方法,即SomeOtherMethod将嵌入其他方法,不会单独存在:publicintCallingMethod(){//codethatperformssometaskA//codethatperformssometaskB//codethatperformssometaskC}精简版JIT编译器只对最基本的方法实施内联,实际上,它只对简单存取器方法(即属性)起作用。方法是否可以内联有一定的评判标准1。注意,方法内联是在内部实现的被动的细节变动,不应该刻意在程序中进行内联设计。但可以使某些需要性能的代码尽量简单,以便它们有更多内联机会。然而,有些方法永远也不会被内联,它们便是“虚方法”。2.虚方法技巧虚方法(virtualmethod)是在C#中被标记为virtual和在VisualBasic中标记为Overridable的方法。它们是对象层次设计中的构建模块,使用方便且优雅,并实现了多态(polymorphism,即在子类中重新定义基类方法的功能)。因为重新定义方法的功能是一种开销,所以,在默认情况下不会开启这个功能。JIT编译器对虚方法的调用,比对非虚方法调用多占用大约40%的资源2。虽然我们不推荐基于这种事实进行设计,但值得一提的是,虚方法不会内联化。尤其要注意虚属性(virtualproperty)的定义,尽管二者在形式上稍有不同,但实际上也都是方法。请考虑下面两个方法,一个方法会调用虚属性,另一个方法也会以同样的方式调用非虚方法:privateintmyVar=1;publicintMyProperty{get{returnmyVar;}set{myVar=value;}}privateintmyVVar=1;publicvirtualintMyVirtualProperty{get{returnmyVVar;}set{myVVar=value;}}1原书注:内联方法的IL大小必须少于16字节(byte),不能够有分支(如if),不能有局部变量,不能有异常处理,不能有32位浮点参数,还不能有返回值。还有,若方法的参数不止一个,这些参数(在IL中)要按从上到下的顺序访问(译者注:即方法体在IL中要以参数定义顺序对其进行访问)。2原书注:精简版JIT编译器不使用虚函数表(v-table),这表明虚拟方法在第一次调用时必须被解释,而不仅仅是查找。publicvoidTest1(){inttotal=0;for(inti=0;i1000000;i++){total+=this.MyProperty;}MessageBox.Show(total.ToString());}publicvoidTest2(){inttotal=0;for(inti=0;i1000000;i++){total+=this.MyVirtualProperty;}MessageBox.Show(total.ToString());}如果您运行前面的代码,便会注意到Test2的性能不如Test1。在运行MicrosoftWindowsMobile2003StandardEdition的PocketPC设备上,Test1在debug模式下用了240ms,在release模式下用了45ms,而Test2分别用了320ms和190ms。如果代码在debug模式下生成,这个性能差距会小于在release模式下的性能差距(当然,release模式总的来说都比debug模式快)。在debug模式下的这个性能差距,因为虚调用本来就慢;而在release模式下这种差距被拉大,因为非虚属性被内联。5.2.2垃圾回收器垃圾回收器负责为对象分配空间,并在对象不再被引用时释放它们。托管环境编程最重要的理念是无需考虑内存管理。然而,这个理念并没有完全实现:虽然它能够对内存进行管理,但如果在设计应用程序时,一点也不考虑对内存的使用,程序的性能可能会很糟糕。所以,您仍然要考虑到内存的管理,但又必须有别于本地代码。在我们分析程序对内存的使用前,先解释一下垃圾回收器在性能方面所扮演的角色,将有助于您理解。WindowsCE与WindowsMobile的内存管理作为托管程序开发者,如果CLR尽职尽责,您就不必操心内存的管理。话虽如此,但我们还是要指出一些托管程序开发者偶尔会遇到的问题。在Micro