| | | | | | | [文章信息] | | | 作者: | Gregor Noriskin | | 时间: | 2003-08-28 | | 出处: | 微软 | | 责任编辑: | 方舟 | |
| [文章导读] | | | 学习如何找出托管代码性能的最佳方法,以及如何测量托管应用程序的性能...... | |
| |
|
| | | |
|
|
|
|
|
.NET 公共语言运行库
.NET Framework 的核心是公共语言运行库 (CLR)。CLR 为您的代码提供所有的运行时服务:实时[Denver1]编译、内存管理、安全性和大量其他服务。CLR 的设计考虑了高性能的要求。也就是说,您既可以充分利用其性能,也可以不发挥这些性能。
本文将从性能的角度概要介绍公共语言运行库,找出托管代码性能的最佳办法,还将展示如何测量托管应用程序的性能。本文并不打算对 .NET Framework 的性能特点进行全面讨论。根据本文的目的,文中提到的性能方面的内容将会包括吞吐量、可扩展性、启动时间和内存使用量等。
托管数据和垃圾回收器
在以性能为中心的应用程序中使用托管代码时,开发人员最关心的问题之一就是 CLR 内存管理的开销 - 这种管理由垃圾回收器 (GC) 执行。内存管理的开销由与类型实例关联的内存的分配开销、在实例的整个生命周期中内存的管理开销以及当不再需要时内存的释放开销计算得出。
托管分配的开销通常都非常小,在大多数情况下,比 C/C++ malloc 或 new 所需的时间还要少。这是因为 CLR 不需要通过扫描可用列表来查找下一个足以容纳新对象的可用连续内存块,而是一直都将指针保持指向内存中的下一个可用位置。我们可以将对托管堆的分配看作“类似于栈”。如果 GC 需要释放内存才能分配新对象,那么分配可能会引发回收操作。在这种情况下,分配的开销就会比 malloc 和 new 大。固定的对象也会对分配的开销造成影响。固定的对象是指那些 GC 接到指令在回收操作期间不能移动其位置的对象,通常是由于这些对象的地址已传递到本机 API 中。
与 malloc 或 new 不同的是,在对象的整个生命周期中管理内存都会带来开销。CLR GC 区分不同的代,这就意味着它不是每次都回收整个堆。但是,GC 仍然需要了解剩余堆中的活动对象的根是否是堆中正在回收的那部分对象。如果内存中包含的对象具有对其下一代对象的引用,那么在这些对象的生命周期中,管理内存的开销将会非常大。
GC 是代标记和空闲内存的回收器。托管堆包含三代:第 0 代包含所有的新对象,第 1 代包含存活期较长的对象,第 2 代包含长期存活的对象。在释放足够的内存以维持应用程序继续运行的前提下,GC 将尽可能从堆中回收最小的部分。代的回收操作包括对所有下一代的回收,在这种情况下,第 1 代回收也会回收第 0 代。第 0 代的大小会根据处理器缓存的大小和应用程序的分配率动态变化,它用于回收的时间通常都不会超过 10 毫秒。第 1 代的大小根据应用程序的分配率动态变化,它用于回收的时间通常介于 10 到 30 毫秒之间。第 2 代的大小取决于应用程序的分配配置文件,它用于回收的时间也取决于此文件。对管理应用程序内存的性能开销影响最大的正是这些第 2 代的回收操作。
提示:GC 具备自我调节能力,它会根据应用程序内存的要求对自身进行调节。多数情况下,通过编程方式调用 GC 时,它的调节功能都未启用。通过调用 GC.Collect 来“帮助”GC 通常无法提高您的应用程序的性能。
GC 可以在回收对象期间重定位存活的对象。如果这些对象比较大,那么重定位的开销也会较大,因此这些对象都将分配到堆中的一个称为大对象堆 (Large Object Heap) 的特殊区域中。大对象堆可以被回收,但不能压缩,例如,这些大对象不能进行重定位。大对象是指那些大于 80k 的对象。请注意,在将来发行的 CLR 版本中此概念可能会有所变化。需要回收大对象堆时,将强制进行完全回收,而且是在回收第 2 代时才回收它们。大对象堆中对象的分配率和死亡率都会对管理应用程序内存的性能开销产生很大的影响。
分配配置文件
托管应用程序的全局分配配置文件定义了垃圾回收器对与应用程序相关的内存进行管理的工作量有多大。GC 管理内存的工作量越大,GC 所经历的 CPU 周期数就越多,而 CPU 运行应用程序代码所花费的时间也就越短。分配配置文件由已分配对象数、对象的大小及其生命周期计算得出。缓解 GC 压力的一种最明显的方法就是减少分配的对象数量。使用面向对象设计技术将应用程序设计为具有可扩展性、模块化和可复用的特性,往往会导致分配的数量增多。抽象和“精确”都会导致性能下降。
GC 友好的分配配置文件中将包含一些在开始执行应用程序时分配的对象,这些对象的生命周期与应用程序一致,而其他对象的生命周期都比较短。存活时间较长的对象很少或不包含对存活时间较短的对象的引用。当分配配置文件偏离此原则时,GC 就必须努力工作以管理应用程序的内存。
GC 不友好的分配配置文件中将包含一些可以存活到第 2 代但随后就会死去的对象,或者将包含一些要分配到大对象堆的存活时间较短的对象。那些存活时间长得足以进入第 2 代而随后就会死去的对象,是管理开销最大的对象。就像我在前面提到的,如果在执行 GC 期间上一代中的对象包含对新一代对象的引用,也会增加回收的开销。
典型的实际分配配置文件介于上面提到的两种分配配置文件之间。分配配置文件的一个重要度量标准是 CPU 花在 GC 上的时间占其总时间的百分比。您可以通过 .NET CLR Memory:% Time in GC 性能计数器获得这一数字。如果此计数器的平均值大于 30%,则您可能需要对您的分配配置文件进行一次仔细的检查。这并不一定意味着您的分配配置文件有问题,在某些占用大量内存的应用程序中,GC 达到这种水平是必然的,也是正常的。当您遇到性能问题时,首先应该检查此计数器,它将立即显示出您的分配配置文件是否出现了问题。
提示:如果 .NET CLR Memory:% Time in GC 性能计数器指示您的应用程序花在 GC 上的平均时间高于它的总时间的 30%,则表明您需要对您的分配配置文件进行一次仔细的检查。
提示:GC 友好的应用程序中包含的第 0 代对象远远多于第 2 代对象。此比率可以通过比较 NET CLR Memory:# Gen 0 Collections 和 NET CLR Memory:# Gen 2 Collections 性能计数器的结果来得出。
|
|
|
|
|
|
|
|