-- 作者:admin
-- 发布时间:11/9/2004 2:26:00 AM
-- 也谈.Net中的效率问题
发信人: flier (小海->找啊找工作 :)), 信区: DotNET 标 题: 也谈.Net中的效率问题 发信站: BBS 水木清华站 (Mon Apr 8 02:26:01 2002) 也谈.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) 6.各种小细节 算了,懒得写了,睡觉了…… :) 上面大概从较高的层面对.Net中效率问题进行了一些介绍, 以后有时间再写一篇从较低的代码层面的解决思路。 -- . 生命的意义在于 /\ ____\ /\_ \ /\_\ . . 希望 \ \ \___/_\/\ \ \/_/__ __ _ _★ . . 工作 \ \ ____\\ \ \ /\ \ /'__`\ /\`'_\ . . 爱你的人 \ \ \___/ \ \ \___\ \ \/\ __//\ \ \/ . . 和你爱的人 \ \___\ \ \_____\ \__\ \____\ \ \_\ . . …… \/___/ \/_____/\/__/\/____/ \/_/ @126.com. ※ 来源:·BBS 水木清华站 smth.org·[FROM: 61.183.136.8] 返回上一页 回到目录 回到页首 下一篇
|