生命无常
如果没有明天

 
 
也谈.Net中的效率问题(草稿) 
 
Flier Lu 
 
前面有朋友提到.Net中的效率问题, 
我这里就我的一些了解结合他的关注点做一些介绍, 
希望对大家提高对.Net的认识有帮助 :) 
 
首先是.Net代码的执行效率。如果单单只是代码运行的话, 
因为.Net是通过JIT编译成本地代码后执行,因此执行效率 
并不比普通的VC/Delphi程序慢多少,比如MS做评测 
C#的XML库实现效率比VC还要高,当然这是特殊情况。 
 
而实际上很多朋友反映.Net程序运行很慢,的确如此, 
为什么呢?如何避免呢? 
 
因为.Net程序与普通程序不同,他首要的任务是保证代码的 
稳定性,因此将类型检查、缓冲区溢出检查、数组越界检查、 
数学运算溢出检查,加上GC、安全认证等等,效率不低才怪。 
这些新特性在提高代码质量的同时是以运行时效率作为代价的。 
 
如果你不需要这些特性,强烈建议你使用Unmanaged C++ 
将核心代码以传统形式开发编译执行,只是在对效率不敏感的 
部分使用.Net来实现。这种方式更多的是需要了解两种C++ 
之间的不同与交互,我们以后有时间再详谈。 
 
这里我关注的是如何提高必须使用.Net架构程序的运行时效率。 
这方面需要关注的几点前面那位朋友其实基本上都提到了 
 
1.自动内存管理,也就是GC 
2.配件Assembly的装载 
3.JIT编译的效率 
4.各种运行时的检测 
5.安全权限验证 
6.各种小细节 
 
因为这里不是写书,我只能各个点大概提一下,有兴趣的朋友 
可以自行查阅相关资料了解详情。 
 
1.自动内存管理,也就是GC 
 
现代编程语言发展的一个重要趋势是使用自动内存管理,也就是GC 
 
来摆脱依赖程序员人的因素的手工内存管理机制,从最开始的 
智能指针,到基于缓冲池的内存管理,到GC,一步一步在发展。 
 
GC的好处与坏处我这里就不多说了,大概介绍一下GC在.Net中的实现机制, 
以及带来的对效率方面的影响好了,说不清的可以re此文和我讨论。 
 
在.Net中,MS实际上是为GC做了相对多的效率上的优化的。 
 
首先,在GC中,分配内存无需象传统C++/Delphi那样遍历一个可用内存链表, 
而是有一个指针执行Managed Heap的使用到的末尾,直接通过指针+Size 
来分配内存,效率达到最高。当然如果没有足够内存要引发强制性GC就ft了, 
因此在进行注重效率部分之前,最好提前分配好需要用的对象、数组等等。 
或者在之前使用GC.GetTotalMemory估算内存是否足够使用。 
如果实在不够可以事先强制性GC.Collect()回收内存 
总之,尽量避免在分配堆内存时引发GC。 
 
注意:只有引用类型如类、数组才在堆里分配内存,值类型如结构、int等是 
直接在堆栈里面分配空间。 
 
其次,在GC中,所有实现了Object.Finalize()方法的类,在析构时需要 
进行特殊处理。在C#里面使用类似C++析构函数的诸如 ~MyClass() 实现。 
普通的对象GC一旦发现其不可到达,马上释放其内存;实现了Object.Finalize() 
方法的对象,则被丢入一个列表,在第一遍GC完成之后逐个调用Finalize函数 
再放回GC的队列中,在第三次时才被真正释放。 
 
因此如果没有必要,绝对不要使用Object.Finalize()图方便,如果实在要 
释放unmanaged resource,加一个Close或者Dispose方法好了,手工处理 
 
此外如果频繁使用或者创建麻烦的对象,可以采用对象池的方法大大提高效率, 
在其析构时放回对象池,使用时直接从池里拿,也可以大大提高效率。 
过两天有时间我给个例子再说…… 
 
再就是GC的分代机制,把所有对象分为若干代Generation, 
用GC.MaxGeneration可以取得最大代数,一般为3代,0-2 
.Net的GC的实现思路是越晚建立的对象越可能被先释放 
因此他将堆分为三代,每次缺省只处理第0代,所有不可访问对象回收, 
可访问对象代数加一。这样就大大优化了GC时的工作量 
我们也可以使用此机制,避免强制性的GC.Collect()回收全部堆 
而一次只操作一代如GC.Collect(0); 
 
再就是比较高级的优化手段弱引用,Weak References, 
这里就不多说了,以后有时间再详谈 
 
2.配件Assembly的装载 
 
配件Assembly的装载也是影响效率的一大问题,特别是动态装载, 
对效率影响更大。因为在装载配件时,CLR要分析配件的Metadata 
要对不同配件之间进行关联,要检查配件的类型安全性,加上后面 
要提到的进行静态安全验证等等,速度还是很慢的。 
 
因此要减少动态的配件使用,尽量用静态连接,在程序开始时完成这些 
麻烦事情。对实在要动态装载的配件,尽量把公共部分提取出来等等。 
当然就算是静态连接,在载入配件时还是要花费相当时间,一个简单 
的解决办法就是在程序开始对效率没有要求时,写一些无用的代码 
去调用配件中等会要在注重效率代码中用到的类,强制CLR载入。 
比如获取配件中类的类型信息等等 
 
3.JIT编译的效率 
 
JIT编译来说,是用到哪里编译到哪里,编译后代码在内存中缓存起来 
因为一次编译代码不多,每次效率较高;同时也因为编译次数较多, 
外加的负荷也较重。 
 
不过解决办法也很简单,用ngen工具强制JIT预编译.Net程序的本地 
代码镜像,并以文件形式缓存起来,这样既可以享受.Net程序的诸多优势, 
又可以享受本地代码直接运行的高效率。 
 
实际上.Net框架里面很多类都是被ngen预处理过的,保证其效率。 
可以用ngen /show命令查看已经被预编译的配件。 
 
4.各种运行时的检测 
 
就JIT编译后代码来说,很大的影响效率的问题是各种运行时检测。 
有些是不可避免的,如类型安全、数组越界;有些是可以选择的, 
如数学运算溢出检查、缓冲区溢出检测等等,可以根据需要关闭之。 
这种检测对计算密集型的程序打击最大,往往一个很简单的算法 
里面被加进了一堆乱七八糟的东东。最简单的解决办法就是用 
unmanaged c++来写,避开这些检测。 
 
5.安全权限验证 
 
安全权限验证对企业级应用来说至关重要,但其对效率的影响与其 
功能强大成反比。比如你要检测一个方法调用者的权限,可能就需要 
遍历整个stack list,检测每个调用者是否有相应权限, 
整个过程耗时巨大。 
 
解决办法一是尽量少用,只在必要的地方用。二是权限尽量限制窄。 
三是尽量以静态验证(以attribute方式声明)代替动态验证 
(以普通类形式使用),将大部分工作放到载入配件时 
 
此外还可以使用一些高级的优化手段,如指定验证的范围, 
缺省是堆栈列表中所有调用者,可以指定到一层,大大减少 
消耗时间,但代价是安全性降低,可能出现安全漏洞 
(程序A使用有权限的程序B来调用程序A没有权限访问的程序C) 

posted on 2005-11-21 10:16  John  阅读(354)  评论(0)    收藏  举报