【MSDN文摘】编写高性能的托管应用程序:入门

By | 04月24日
Advertisement

编写高性能的托管应用程序:入门

发布日期: 6/15/2004 | 更新日期: 6/15/2004

Gregor Noriskin

Microsoft CLR 性能小组

适用于:Microsoft .NET 框架

摘要:从性能的角度来学习 .NET 框架公共语言运行库。学习如何找出托管代码性能的最佳做法,以及如何测量托管应用程序的性能。

【MSDN文摘】编写高性能的托管应用程序:入门


下载 CLR Profiler。(330KB)

本页内容
【MSDN文摘】编写高性能的托管应用程序:入门
软件开发如同玩杂耍
【MSDN文摘】编写高性能的托管应用程序:入门
.NET 公共语言运行库
【MSDN文摘】编写高性能的托管应用程序:入门
托管数据和垃圾回收器
【MSDN文摘】编写高性能的托管应用程序:入门
分配配置文件
【MSDN文摘】编写高性能的托管应用程序:入门
用于分析的 API 和 CLR 分析器
【MSDN文摘】编写高性能的托管应用程序:入门
宿主服务器 GC
【MSDN文摘】编写高性能的托管应用程序:入门
终结 (Finalization)
【MSDN文摘】编写高性能的托管应用程序:入门
处置模式 (Dispose Pattern)
【MSDN文摘】编写高性能的托管应用程序:入门
弱引用注释
【MSDN文摘】编写高性能的托管应用程序:入门
托管代码和 CLR JIT
【MSDN文摘】编写高性能的托管应用程序:入门
值类型
【MSDN文摘】编写高性能的托管应用程序:入门
异常处理
【MSDN文摘】编写高性能的托管应用程序:入门
线程和同步
【MSDN文摘】编写高性能的托管应用程序:入门
反射 (Reflection)
【MSDN文摘】编写高性能的托管应用程序:入门
晚期绑定
【MSDN文摘】编写高性能的托管应用程序:入门
安全性
【MSDN文摘】编写高性能的托管应用程序:入门
COM 互操作和平台调用
【MSDN文摘】编写高性能的托管应用程序:入门
性能计数器
【MSDN文摘】编写高性能的托管应用程序:入门
其他工具
【MSDN文摘】编写高性能的托管应用程序:入门
小结
【MSDN文摘】编写高性能的托管应用程序:入门
资源

软件开发如同玩杂耍

我 们可以将软件开发过程比作玩杂耍。通常,人们在玩杂耍时至少需要三件物品,而对于可以使用的物品数量并没有任何限制。刚开始学习如何玩杂耍时,您会发现在 接球和抛球时您看到的是单个的球。随着熟练程度的增加,您会开始将注意力集中在这些球的运动上,而不是集中在每个单个的球上。当您掌握了玩杂耍的方法时, 您就会再次将注意力集中在单个球上,一边努力使这个球保持平衡,一边继续抛出其他的球。凭直觉您就可以知道这些球的运动方向,并且也总是能将手放在正确的 地方接球和抛球。可是,这与软件开发又有什么类似之处呢?

不同的角色在软件开发的过程中会“耍弄”不同的三件物品:项目和计划经理耍弄的是 功能、资源和时间,而软件开发人员则耍弄正确性、性能和安全性。人们在玩杂耍时总是试图使用更多的物品,但只要是学过杂耍的人都会知道,即使只添加一个球 都会让在空中保持所有球的平衡变得更加困难。从技术上讲,如果耍弄的球少于三个,那根本就不叫杂耍。作为软件开发人员,如果您不考虑正在编写的代码的正确 性、性能和安全性,则说明您根本就没有投入到您的工作中。当您刚刚开始考虑正确性、性能和安全性时,您会发现您每次只能将注意力集中在一个方面。当它们成 为您日常工作的一部分时,您会发现您再也不需要将注意力集中在某一个特定的方面,因为此时它们早已融入了您的工作。而一旦掌握了它们,您就能够凭直觉进行 权衡,并相应地调整自己的注意点。这就跟玩杂耍一样,关键在于实践。

编写高性能的代码本身也存在三件物品:设置目标、测量和了解目标平台。 如果您不知道代码必须达到的运行速度,那么您又如何知道您完成了呢?如果您不测量和分析代码,那么您又如何知道您已经达到了目标,或为什么没有达到目标 呢?如果您不了解目标平台,那么当您没有实现目标时,又如何知道要对什么进行优化呢?这些原则对所有的高性能代码开发过程几乎都适用,不管您以哪种平台作 为目标平台都一样。要完成一篇有关编写高性能代码的文章,就必须提到以上这三个方面。虽然所有这三个方面都同等重要,但是本文的重点在于后两个方面,因为 它们适用于编写以 Microsoft? .NET 框架作为目标平台的高性能应用程序。

在任意平台上编写高性能代码的基本原则为:


1.


设置性能目标


2.


测量,测量,再测量


3.


了解应用程序的目标硬件和软件平台

【MSDN文摘】编写高性能的托管应用程序:入门
返回页首

.NET 公共语言运行库

.NET 框架的核心是公共语言运行库 (CLR)。CLR 为您的代码提供所有的运行时服务:实时编译、内存管理、安全性和大量其他服务。CLR 的设计考虑了高性能的要求。也就是说,您既可以充分利用其性能,也可以不发挥这些性能。

本文将从性能的角度概要介绍公共语言运行库,找出托管代码性能的最佳办法,还将展示如何测量托管应用程序的性能。本文并不打算对 .NET 框架的性能特点进行全面讨论。根据本文的目的,文中提到的性能方面的内容将会包括吞吐量、可扩展性、启动时间和内存使用量等。

【MSDN文摘】编写高性能的托管应用程序:入门
返回页首

托管数据和垃圾回收器

在 以性能为中心的应用程序中使用托管代码时,开发人员最关心的问题之一就是 CLR 内存管理的开销 - 这种管理由垃圾回收器 (GC) 执行。内存管理的开销由与类型实例关联的内存的分配开销、在实例的整个生命周期中内存的管理开销以及当不再需要时内存的释放开销计算得出。

托 管分配的开销通常都非常小,在大多数情况下,比 C/C++ malloc 或 new 所需的时间还要少。这是因为 CLR 不需要通过扫描可用列表来查找下一个足以容纳新对象的可用连续内存块,而是一直都将指针保持指向内存中的下一个可用位置。我们可以将对托管堆的分配看作 “类似于堆栈”。如果 GC 需要释放内存才能分配新对象,那么分配可能会引发回收操作。在这种情况下,分配的开销就会比 mallocnew 大。固定的对象也会对分配的开销造成影响。固定的对象是指那些 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 代时才回收它们。大对象堆中对象的分配率和死亡率都会对管理应用程序内存的性能开销产生很大的影响。

【MSDN文摘】编写高性能的托管应用程序:入门
返回页首

分配配置文件

托 管应用程序的全局分配配置文件定义了垃圾回收器对与应用程序相关的内存进行管理的工作量有多大。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 性能计数器的结果来得出。

【MSDN文摘】编写高性能的托管应用程序:入门
返回页首

用于分析的 API 和 CLR 分析器

CLR 中包含一个功能强大的用于分析的 API,第三方可用它来为托管应用程序编写自定义分析器。CLR 分析器是一种分配分析示例工具,由 CLR 产品小组编写,但不提供技术支持,该分析器使用的就是这种用于分析的 API。开发人员可以使用 CLR 分析器查看其管理应用程序的分配配置文件。

【MSDN文摘】编写高性能的托管应用程序:入门

1CLR 分析器主窗口

CLR 分析器包括大量非常有用的分配配置文件视图,包括:已分配类型的柱状图,分配和调用图表,显示不同代的 GC 和进行这些回收之后托管堆的结果状态的时间线,以及显示各个方法分配和程序集加载情况的调用树。

【MSDN文摘】编写高性能的托管应用程序:入门

2CLR 分析器分配图表

提示有关如何使用 CLR 分析器的详细信息,请参阅 Zip 文件中包含的自述文件。

请注意,CLR 分析器具有高性能的特点,可以显著地改变应用程序的性能特点。如果您在运行应用程序时也运行了 CLR 分析器,因压力而产生的问题很可能都会消失。

【MSDN文摘】编写高性能的托管应用程序:入门
返回页首

宿主服务器 GC

有 两种不同的垃圾回收器可供 CLR 使用:工作站 GC 和服务器 GC。控制台和 Windows 窗体应用程序中宿主了工作站 GC,而 ASP.NET 中宿主了服务器 GC 。服务器 GC 针对吞吐量和多处理器的可扩展性进行了优化。服务器 GC 在整个回收期间(包括标记阶段和清除阶段)会暂停所有运行托管代码的线程,并且 GC 会在可供高优先级的 CPU 专用线程中的进程使用的所有 CPU 上并行执行。如果线程在执行 GC 期间运行了本机代码,那么只有当本机调用返回时这些线程才会暂停。如果您要构建的服务器应用程序将要在多处理器计算机上运行,强烈建议您使用服务器 GC。如果您的应用程序不是由 ASP.NET 提供的,那么您就必须编写一个本机应用程序来显式宿主 CLR。

提示如果要构建可扩展的服务器应用程序,请宿主服务器 GC。请参阅 Implement a Custom Common Language Runtime Host for Your Managed App

工 作站 GC 经过优化,其滞后时间非常短,非常适合客户端应用程序。没有人会希望客户端应用程序在执行 GC 期间出现明显的停顿,这是因为客户端的性能通常都不是通过原始吞吐量而是通过反应性能来测量的。工作站 GC 是并发 GC,这意味着它会在托管代码运行的同时执行标记阶段。工作站 GC 仅在需要执行清除阶段时才会暂停运行托管代码的线程。在工作站 GC 中,由于 GC 仅在一个线程上运行,因而它只在一个 CPU 上运行。

【MSDN文摘】编写高性能的托管应用程序:入门
返回页首

终结 (Finalization)

CLR 提供一种在释放与类型实例关联的内存之前自动进行清理的机制。这一机制称为终结 (Finalization)。通常,终结用于释放本机资源,在这种情况下,释放由对象使用的数据库连接或操作系统句柄。

终 结是一个开销很大的功能,而且它还会加大 GC 的压力。GC 会跟踪 Finalizable 队列中需要执行终结操作的对象。如果在回收期间,GC 发现了一个不再存活且需要终结的对象,它就会将该对象在 Finalizable 队列中的条目移至 FReachable 队列中。终结操作在一个称为终结器线程 (Finalizer Thread) 的独立线程中执行。因为在终结器的执行过程中,可能需要用到该对象的所有状态,因此该对象或其指向的所有对象都会升级至下一代。与对象或对象图相关的内存 仅在执行后续的 GC 时才会释放。

需要释放的资源应该包装在一个尽可能小的可终结对象中,例如,如果您的类需要引用托管资源和非托管资 源,那么您应该在新的可终结类中包装非托管资源,并使该类成为您的类的成员。父类不能是可终结类。这意味着只有包含非托管资源的类会被升级(假如您没有在 包含非托管资源的类中引用父类)。另外还要记住只有一个终结线程。如果有终结器导致此线程被阻塞,就不能调用后续的终结器,也不能释放资源,而且您的应用 程序会导致资源泄漏。

提示应该尽可能使终结器保持简单,且永远不会阻塞。

提示仅将需要清理的非托管对象的包装类设为可终结。

可 以将终结认为是引用计数的一种替代形式。执行引用计数的对象将跟踪有多少其他对象对其进行引用(这会导致一些非常众所周知的问题),以便在引用计数为零时 释放其资源。CLR 没有实现引用计数,因此它需要提供一种机制,以便在不存在对象的引用时自动释放资源。终结就是这种机制。通常,终结仅需要在要清理的对象的生命周期不明确 的情况下执行。

【MSDN文摘】编写高性能的托管应用程序:入门
返回页首

处置模式 (Dispose Pattern)

当 对象的生命周期不明确时,应该尽快释放与该对象关联的非托管资源。这一过程称为“处置”对象。处置模式通过 IDisposable 接口实现(尽管您自己实现也很容易)。如果您希望对类应用终结,例如,要使类实例可处置,就需要让对象实现 IDisposable 接口并执行 Dispose 方法。使用 Dispose 方法您可以调用终结器中的同一段清理代码,并通知 GC 不需要通过调用 GC.SuppressFinalization 方法来终结该对象。最好同时使用 Dispose 方法和终结器来调用通用终结函数,这样就只需要维护一份清理代码。而且,如果对象的语义是这样的:Close 方法比 Dispose 方法更符合逻辑,那么还应实现 Close 方法,在这种情况下,数据库连接或套接字逻辑上都被“关闭”。Close 可以只是调用 Dispose 方法。

使用终结器为类提供 Dispose 方法始终是一种很好的做法,因为永远没有人能确切地知道使用类的方法,例如,是否可以明确知道它的生命周期。如果您使用的类实现了处置模式,而且您也确切地知道何时处置好对象,则最好明确地调用 Dispose

提示请为所有可终结的类提供 Dispose 方法。

提示请在 Dispose 方法中取消终结操作。

提示请调用通用清理函数。

提示如果您使用的对象实现了 IDisposable,并且您知道已不再需要该对象,请调用 Dispose 方法。

C# 提供了一种非常方便的自动处置对象的方法。使用关键字 using 来标记代码块,之后,将对大量可处置对象调用 Dispose

C# using 关键字

using(DisposableType T){ //Do some work with T}//T.Dispose() is called automatically

【MSDN文摘】编写高性能的托管应用程序:入门
返回页首

弱引用注释

在 堆栈中、寄存器中、其他对象上或某一其他 GC 根对象上,对对象的任何引用都会使得该对象在执行 GC 期间保持存活。一般来说,这是一件好事,因为这通常都表示应用程序不是借助该对象来执行的。然而,有些时候您会需要引用某个对象,但又不想影响其生命周 期。在这种情况下,CLR 为实现这一目的而提供了一种称为“弱引用”的机制。任何强引用(例如,以对象为根的引用)都可以转换成弱引用。例如,当您需要创建可以遍历数据结构的外部 游标对象,但又不影响该对象的生命周期时,可能需要使用弱引用。又例如,当您需要创建一个存在内存压力时就会刷新的缓存时也可能会需要使用弱引用,例如, 发生 GC 时。

C# 中创建弱引用

MyRefType mrt = new MyRefType();//...//Create weak referenceWeakReference wr = new WeakReference(mrt); mrt = null; //object is no longer rooted//...//Has object been collected?if(wr.IsAlive){   //Get a strong reference to the object   mrt = wr.Target;   //object is rooted and can be used again}else{   //recreate the object   mrt = new MyRefType();}

【MSDN文摘】编写高性能的托管应用程序:入门
返回页首

托管代码和 CLR JIT

托 管程序集是托管代码的分发单位,它由 Microsoft 中间语言(MSIL 或 IL)构成,适用于所有的处理器。CLR 的实时 (JIT) 功能可将 IL 编译成优化的本机 X86 指令。JIT 是一种执行优化操作的编译器,但是由于编译是在软件运行时进行的,并且仅当第一次调用方法时才会进行,因此进行优化的次数需要与执行编译所花费的时间保持 平衡。通常,这对于服务器应用程序并不重要,因为启动时间和响应对于它们来说通常都不构成问题;但对于客户端应用程序来说,却十分重要。请注意,安装时可 以通过使用 NGEN.exe 执行编译来加快启动时间。

许多由 JIT 执行的优化操作都没有与其关联的编程模式,例如,您无法对它们进行显式编码,但是也有一些优化操作具有关联的编程模式。下一节将讨论后者中的部分操作。

提示使用 NGEN.exe 实用工具在安装时编译应用程序,可以加快客户端应用程序的启动时间。

方法内联

所 有的方法调用都会带来开销。例如,需要将参数推入堆栈中或存储在寄存器中,需要执行的方法起头 (prolog) 和结尾 (epilog) 等。只需要将被调用方法的方法主体移入调用方的主体,就可以避免某些方法的调用开销。这一操作称为方法内联。JIT 使用大量的探测方法来确定是否应内联某个方法。下面是其中一些比较重要的探测方法的列表(请注意这并不是一个详尽的列表):


IL 超过 32 字节的方法不会内联。


虚函数不会内联。


包含复杂流程控制的函数不会内联。复杂流程控制是除 if/then/else 以外的任意流程控制,在这种情况下,为 switch 或 while。


包含异常处理块的方法不会内联,但是引发异常的方法可以内联。


如果某个方法的所有定参都为结构,则该方法不会内联。

我会认真考虑一下对这些探测方法进行显式编码的问题,因为在以后的 JIT 版本中它们可能会有所变化。请不要为了确保方法可以内联而放弃方法的正确性。您也许已经注意到了一个有趣的现象,C++ 中的关键字 inline __inline不能保证编译器将一种方法内联(尽管 __forceinline可以)。

一般情况下,属性的 Get 和 Set 方法都非常适合内联,因为它们主要用于初始化私有数据成员。

提示请不要为了试图保证内联而放弃方法的正确性。

去除范围检查

托 管代码有许多优点,其中一项是可以自动进行范围检查。每次使用 array[index] 语义访问数组时,JIT 都会进行检查以确保索引在数组范围中。在具有大量迭代和少量由每个迭代执行的指令的循环环境中,范围检查的开销可能会很大。在某些情况下,JIT 也可能会在检测到这些范围检查不必要时将其从循环体中删除,即仅在循环开始执行之前对其进行一次检查。在 C# 中有一种编程模式,用来确保这些范围检查会被删除:对“for”语句中数组的长度进行的显式测试。请注意,只要此模式中存在细微的偏差都会导致检查无法去 除,在这种情况下,需向索引中添加一个值。

C# 中去除范围检查

//Range check will be eliminatedfor(int i = 0; i < myArray.Length; i++) {   Console.WriteLine(myArray[i].ToString());}//Range check will NOT be eliminatedfor(int i = 0; i < myArray.Length + y; i++) {    Console.WriteLine(myArray[i+x].ToString());}

搜索大型不规则数组时,优化操作特别明显,因为此时将同时删除内循环和外循环范围检查。

要求进行变量使用情况跟踪的优化操作

大 量 JIT 编译器优化操作都要求 JIT 跟踪定参和局部变量的使用情况,例如,它们在方法主体中的最早以及最近一次使用的时间。在 CLR 1.0 和 1.1 版中,JIT 可以跟踪使用情况的变量总数限制在 64 个之内。例如“寄存操作”就是一个需要进行使用情况跟踪的优化操作。寄存操作是指将变量存储在处理器寄存器中,而不是堆栈框架中(例如,在内存中)。与对 堆栈框架中的变量进行访问的时间相比,对寄存变量的访问要快得多,即便框架中的变量位于处理器的缓存中也一样。只能对 64 个变量进行寄存,所有其他变量都将推至堆栈中。除寄存操作以外,另外也有一些优化操作需要进行使用情况跟踪。应该将方法中的定参和局部参数的数量保持在 64 个以下,以确保实现最大数目的 JIT 优化操作。请记住在以后的 CLR 版本中此数目可能会有所变化。

提示使方法保持简短。要这样做的原因有很多,包括方法内联、寄存操作和 JIT 持续时间的需要。

其他 JIT 优化操作

JIT 编译器还可以执行大量其他的优化操作:常量和副本传播、循环不变式提升以及若干其他操作。需要用来实现优化的编程模式都是免费的,无需花钱购买。

为什么我在 Visual Studio 中没有看到这些优化功能?

当 您在 Visual Studio 中使用 Debug(调试)菜单或按下 F5 键启动应用程序时,无论您生成的是发行版还是调试版,所有的 JIT 优化功能都将被禁用。当托管应用程序通过调试器启动时,即使它不是该应用程序的调试版本,JIT 也会发出非优化的 x86 指令。如果您希望 JIT 发出优化代码,那么请从 Windows 资源管理器中启动该应用程序,或者在 Visual Studio 中使用 CTRL+F5 组合键。如果希望查看优化的反汇编程序,并将其与非优化代码进行对比,则可以使用 cordbg.exe。

提示使用 cordbg.exe 可以查看 JIT 发出的优化和非优化代码的反汇编程序。使用 cordbg.exe启动应用程序之后,可以通过键入下列代码设置 JIT 模式:

(cordbg) mode JitOptimizations 1JIT's will produce optimized code(cordbg) mode JitOptimizations 0

JIT 将生成可调试(非优化)代码。

【MSDN文摘】编写高性能的托管应用程序:入门
返回页首

值类型

CLR 可提供两组不同的类型:引用类型和值类型。引用类型总是分配到托管堆中,并按引用传递(正如它的名称所暗示的)。值类型分配到堆栈中或在堆中内联为对象的 一部分,默认情况下按值传递,不过您也可以按引用来传递它们。分配值类型时,所需的开销非常小,假设它们总是又小又简单,当它们作为参数进行传递时开销也 会很小。正确使用值类型的一个很好的示例就是包含 xy 坐标的 Point 值类型。

Point 值类型

struct Point{   public int x;   public int y;   //}

值类型也可以视为对象,例如,可以在值类型上调用对象方法,它们可以转换为对象,或传递到需要使用对象的位置。无论使用什么方法,只要 将值类型转换为引用类型,都需要经过装箱 (Boxing) 处理。对值类型进行装箱处理时,会在托管堆中分配一个新对象,而值则复制到此新对象中。这项操作将占用很大的系统开销,还可能会降低或完全消除通过使用值 类型而获得的性能。将装箱的类型隐式或显式转换回值类型的过程称为取消装箱 (Unboxed)。

装箱/取消装箱值类型

C#

int BoxUnboxValueType(){ int i = 10; object o = (object)i; //i is Boxed return (int)o + 3; //i is Unboxed}

MSIL

.method private hidebysig instance int32 BoxUnboxValueType() cil managed{ // Code size 20 (0x14) .maxstack 2 .locals init (int32 V_0, object V_1) IL_0000: ldc.i4.s 10 IL_0002: stloc.0 IL_0003: ldloc.0 IL_0004: box [mscorlib]System.Int32 IL_0009: stloc.1 IL_000a: ldloc.1 IL_000b: unbox [mscorlib]System.Int32 IL_0010: ldind.i4 IL_0011: ldc.i4.3 IL_0012: add IL_0013: ret} // end of method Class1::BoxUnboxValueType

如果实现自定义值类型(C# 中的结构),则应该考虑覆盖 ToString 方法。如果不覆盖此方法,那么对值类型上的 ToString 的调用将导致该类型被装箱。对于从 System.Object 继承的其他方法也是如此。在这种情况下,请使用 Equals 来进行覆盖,尽管 ToString 很可能是最常用的调用方法。如果您希望了解值类型是否被装箱以及何时装箱,可以使用 ildasm.exe 实用工具在 MSIL 中查找 box 指令(如上所述)。

覆盖 C# 中的 ToString() 方法以防止装箱

struct Point{ public int x; public int y; //This will prevent type being boxed when ToString is called public override string ToString() { return x.ToString() + "," + y.ToString(); }}

请注意,在创建集合(例如,浮点数组列表)时,添加到集合中的每一项都将进行装箱。您应该考虑使用数组或为值类型创建自定义集合类。

使用 C# 中的集合类时进行隐式装箱

ArrayList al = new ArrayList();al.Add(42.0F); //Implicitly Boxed becuase Add() takes objectfloat f = (float)al[0]; //Unboxed

【MSDN文摘】编写高性能的托管应用程序:入门
返回页首

异常处理

通 常,错误条件都将作为常规流程控制使用。在此情况下,如果试图通过编程将用户添加到 Active Directory 实例中,则只能试着添加该用户,如果系统返回 E_ADS_OBJECT_EXISTS HRESULT,则说明它们已经存在于该目录中。此外,您也可以通过搜索目录查找该用户,如果搜索失败则只需添加该用户。

按照常规流程控制 使用错误,在 CLR 环境中会降低性能。CLR 中的错误处理是借助结构化异常处理实现的。引发异常之前,托管异常的开销非常小。在 CLR 中,引发异常时,需要使用堆栈遍历为已引发的异常找到相应的异常处理程序。堆栈遍历是一种开销较大的操作。正如它的名称所表示的,异常应该用于异常或意外 的情况。


提示对于以性能为中心的方法,请考虑返回预期结果的枚举结果,而不是引发异常。


提示有多种 .NET CLR 异常性能计数器都可以通知您在应用程序中引发了多少异常。


提示如果您使用 VB.NET 的 use exceptions 而不是 On Error Goto,错误对象就是不必要的开销。

【MSDN文摘】编写高性能的托管应用程序:入门
返回页首

线程和同步

CLR 提供丰富的线程和同步功能,包括创建自己的线程、线程池和各种同步原语的能力。在充分利用 CLR 中支持的线程之前,应该仔细考虑一下线程的用法。请记住,添加线程实际上会降低吞吐量,而不会增加吞吐量,但它肯定会增加内存的利用率。在将要在多处理器 计算机上运行的服务器应用程序中,采用并行操作(尽管这取决于将要执行多少锁争用,例如,序列化执行方式)来添加线程可以显著地提高吞吐量;在客户端应用 程序中,添加显示活动和/或进度的线程可以提高反应性能(低吞吐量开销)。

如果应用程序中的线程不是专门用于特定任务的线程,或者关联有特 殊的状态,则应该考虑使用线程池。如果您过去使用过 Win32 线程池,那对 CLR 线程池也一定会比较熟悉。每个托管进程仅存在一个线程池实例。线程池可以智能地识别出它所创建的线程数量,并且会根据计算机上的负载对自身进行调节。

要 讨论线程处理,就必须讨论同步。所有由多线程为应用程序带来的吞吐量收益都可能会因为同步逻辑编写不正确而全部丧失。锁定的粒度会大大影响应用程序的总体 吞吐量,这是因为创建和管理锁会带来系统开销,并且锁定很可能会序列化执行步骤所致。我将使用在树中添加节点的示例来说明这一观点。例如,如果树将成为共 享的数据结构,则多线程需要在执行应用程序期间对其进行访问,而且您需要对该树进行同步访问。您可以选择在添加节点的同时锁定整个树,这意味着您只会在单 一锁定时带来开销,但其他试图访问该树的线程都可能会因此而阻塞。这将是一个粗粒度的锁定示例。或者,您可以在遍历该树时锁定每个节点,这意味着您会在每 个节点上创建锁时带来开销,但是其他线程不会因此而阻塞,除非它们试图访问您已经锁定的特定节点。这是细粒度的锁定示例。仅为要对其进行操作的子树添加锁 也许是更合适的锁定粒度。请注意,在本示例中,您可能会使用共享锁 (RWLock),因为只有这样才能让多个读者同时进行访问。

执行同步操作时最简单有效的方法就是使用 System.Threading.Interlocked 类。Interlocked 类提供大量低级别的原子操作:IncrementDecrementExchangeCompareExchange

C# 中使用 System.Threading.Interlocked 类

using System.Threading;//...public class MyClass{   void MyClass() //Constructor   {      //Increment a global instance counter atomically      Interlocked.Increment(ref MyClassInstanceCounter);   }

~MyClass() //Finalizer   {      //Decrement a global instance counter atomically      Interlocked.Decrement(ref MyClassInstanceCounter);      //...    }   //...}

最常用的同步机制可能是监测器 (Monitor) 或临界区 (Critical Section)。监测器锁可直接使用,也可以借助 C# 中的 lock 关键字来使用。对于给定的对象来说,lock 关键字会对特定的代码块进行同步访问。从性能的角度来说,如果监测器锁的争用率较低,则系统开销相对较小;但是如果其争用率较高,系统开销也会相对较大。

C# lock 关键字

//Thread will attempt to obtain the lock//and block until it doeslock(mySharedObject){   //A thread will only be able to execute the code   //within this block if it holds the lock}//Thread releases the lock

RWLock 提供的是共享锁定机制:例如,该锁可以在“读者”之间共享,但是不能在“作者”之间共享。在这种锁也适用的情况下,使用 RWLock 可以比使用监测器带来更好的吞吐量,它每次只允许一位读者或作者获得该锁。System.Threading 命名空间也包括 Mutex 类。Mutex 是一种同步原语,可用来进行跨进程的同步操作。请注意,它比临界区的开销要大很多,仅当需要进行跨进程的同步操作时才应使用它。

【MSDN文摘】编写高性能的托管应用程序:入门
返回页首

反射 (Reflection)

反射是由 CLR 提供的一种机制,用于在运行时通过编程方式获得类型信息。反射在很大程度上取决于嵌入在托管程序集中的元数据。许多反射 API 都要求搜索并分析元数据,这些操作的开销都很大。

这些反射 API 可以分为三个性能区间:类型比较、成员枚举和成员调用。这些区间的系统开销一直在变大。类型比较操作,在本例中,有 typeof (C#)、以及 GetTypeisIsInstanceOfType 等,都是开销最小的反射 API,尽管它们的实际开销一点也不小。成员枚举操作可以通过编程方式对类的方法、属性、字段、事件、构造函数等进行检查。例如,可能会在设计时的方案中 使用这一类的成员枚举操作,在这种情况下,此操作将枚举 Visual Studio 中的 Property Browser(属性浏览器)的 Customs Web Controls(自定义 Web 控件)的属性。那些用于动态调用类成员或动态发出 JIT 并执行某个方法的反射 API 是开销最大的反射 API。当然,如果需要动态加载程序集、类型实例化以及方法调用,还存在一种晚期绑定方案,但是这种松散的耦合关系需要进行明确的性能权衡。一般情况下, 应该在对性能影响很大的代码路径中避免使用反射 API。请注意,尽管您没有直接使用反射,但是您使用的 API 可能会使用它。因此,也要注意是否间接使用了反射 API。

【MSDN文摘】编写高性能的托管应用程序:入门
返回页首

晚期绑定

晚 期绑定调用是一种在内部使用反射的功能。Visual Basic.NET 和 JScript.NET 都支持晚期绑定调用。例如,使用变量之前您不必进行声明。晚期绑定对象实际上是类型对象,可以在运行时使用反射将该对象转换为正确的类型。晚期绑定调用比 直接调用要慢几个数量级。除非您确实需要晚期绑定行为,否则应该避免在性能关键代码路径中使用它。

提示如果您正在使用 VB.NET,且并不一定需要晚期绑定,您可以在源文件的顶部包含 Option Explicit On 和 Option Strict On 以便通知编译器不允许晚期绑定。这些选项将强制您进行声明,并要求您设置变量类型并关闭隐式转换。

【MSDN文摘】编写高性能的托管应用程序:入门
返回页首

安全性

安 全性是必要的而且也是主要的 CLR 的组成部分,使用它时会降低性能。当代码为 Fully Trusted(完全信任)且安全策略为默认设置时,安全性对应用程序的吞吐量和启动时间的影响会很小。对代码持不完全信任态度(例如,来自 Internet 或 Intranet 区域的代码)或缩小 MyComputer Grant Set 都将增加安全性的性能开销。

【MSDN文摘】编写高性能的托管应用程序:入门
返回页首

COM 互操作和平台调用

COM 互操作和平台调用会以几乎透明的方式为托管代码提供本机 API,通常调用大多数本机 API 时都不需要任何特殊代码,但是可能需要使用鼠标进行多次单击。正如您所预计的,从托管代码中调用本机代码会带来开销,反之亦然。这笔开销由两部分组成:一 部分是固定开销,此开销与在本机代码和托管代码之间进行的转换有关;另一部分是可变开销,此开销与那些可能要用到的参数封送和返回值有关。COM 互操作和平台调用的固定开销在开销中占的比例较小:通常不超过 50 条指令。在各托管类型之间进行封送处理的开销取决于它们在边界两侧的表示形式的相似程度。需要进行大量转换的类型开销相对较大。例如,CLR 中的所有字符串都为 Unicode 字符串。如果要通过平台调用需要 ANSI 字符数组的 Win32 API,则必须缩小该字符串中的每个字符。但是,如果是将托管的整数数组传递到需要本机整数数组的类型中时,就不需要进行封送处理。

由于存 在与调用本机代码相关的性能开销,因此您应该确保该开销是合理的开销。如果您打算进行本机调用,请确保本机调用所做的工作使得因执行此调用而产生的性能开 销划算,即尽量使方法“小而精”而非“大而全”。测量本机调用开销的一种好方法是测量不接受任何参数也不具备任何返回值的本机方法的性能,然后再测量您希 望调用的本机方法的性能。它们之间的差异即封送处理的开销。

提示应创建“小而精”的 COM 互操作和平台调用,而不是“大而全”的调用,并确保调用的开销对于调用的工作量是划算的。

请 注意,不存在与托管线程相关的线程模式。当您打算进行 COM 互操作调用时,需要确保已将执行调用的线程初始化为正确的 COM 线程模式。此操作通常是使用 MTAThreadAttribute 和 STAThreadAttribute 来实现的(尽管也可以通过编程来实现)。

【MSDN文摘】编写高性能的托管应用程序:入门
返回页首

性能计数器

有 大量的 Windows 性能计数器可供 .NET CLR 使用。当开发人员首次诊断性能问题,或试图找出托管应用程序的性能特点时,这些性能计数器就是他们可以选择的武器。我已经简要介绍了几个与内存管理和异常 有关的性能计数器。在 CLR 和 .NET 框架中,性能计数器几乎无处不在。通常,这些性能计数器都可以使用且对系统无害,它们的开销较低,而且不会改变应用程序的性能特征。

【MSDN文摘】编写高性能的托管应用程序:入门
返回页首

其他工具

除 了性能计数器和 CLR 分析器以外,您还需要使用常规的分析器来确定应用程序中的哪些方法花费的时间最多,且最常被调用。这些方法将是您需要最先进行优化的方法。有多种支持托管 代码的商用分析器可供使用,包括 Compuware 的 DevPartner Studio Professional Edition 7.0 和 Intel? 的 VTune? Performance Analyzer 7.0。Compuware 还生产一种免费的托管代码分析器,名为 DevPartner Profiler Community Edition。

【MSDN文摘】编写高性能的托管应用程序:入门
返回页首

小结

本 文只是从性能的角度初步介绍了 CLR 和 .NET 框架。在 CLR 和 .NET 框架中,还有许多别的方面也会对应用程序的性能产生影响。我最希望告诉各位开发人员的是:请不要对您的应用程序的目标平台以及您正在使用的 API 的性能做任何的假设。请测量它们!

祝您成功。

【MSDN文摘】编写高性能的托管应用程序:入门
返回页首

资源

Compuware DevPartner Studio Professional Edition 7.0。

Intel.VTune Performance Analyzer 7.0。

Compuware DevPartner Profiler Community Edition。

Jan Gray,Writing Faster Code:Knowing What Things Cost,MSDN。

Rico Mariani,Garbage Collector Basics and Performance Hints,MSDN。

Similar Posts:

  • 编写高性能Web应用程序的10个技巧(转载)

    使用ASP.NET编写Web应用程序的简单程度令人不敢相信.正因为如此简单,所以很多开发人员就不会花时间来设计其应用程序的结构,以获得更好的性能了.在本文中,我将讲述 10 个用于编写高性能 Web 应用程序的技巧.但是我并不会将这些建议仅局限于 ASP.NET 应用程序,因为这些应用程序只是 Web 应用程序的一部分.本文不作为对 Web 应用程序进行性能调整的权威性指南 - 一整本书恐怕都无法轻松讲清楚这个问题.请将本文视作一个很好的起点. 成为工作狂之前,我原来喜欢攀岩.在进行任何大型攀岩

  • (转)编写高性能Web应用程序的10个技巧(一)

    技巧 1——返回多个结果集 查看一下您的数据库代码,看看您是否有访问数据库多于一次的请求路径(request paths).每个这样的往返都回降低您的应用程序每秒可以服务的请求的数量.通过在一次数据库请求中返回多个结果集,您可以减少数据库通信消耗的总时间.在您减少了数据库服务器管理的请求之后,您也会使您的系统更具可升级性. 一般您可以使用动态SQL语句来返回多个结果集,我更喜欢用存储过程.是否应该把业务逻辑放在存储过程中是存在争议的,但我认为如果一个存储过程中的逻辑可以限制返回的数据(减少数据集

  • 如何编写高性能的服务器程序

    在<High Performence Server Architecture>(http://pl.atyp.us/content/tech/servers.html)中,作者提出了自己的观点关于如何编写高性能的服务器程序.作者感兴趣的地方不在于使用多线程之类的并行技术,作者认为处理请求的基础设施决定了程序的性能.作者也坦言,他提出的观点和方法并不是提高服务器性能的唯一途径.作者总结了四大性能杀手: 数据拷贝 上下文环境切换 内存分配 锁竞争 如果处理请求的操作不会涉及数据拷贝,不会做上下文切

  • 高性能ASP.NET应用程序的探讨与研究

    0 引 言 ASP.NET是微软公司推出的新一代的动态服务器页面,目前的版本是1.0.它是微软的.NET框架结构中的一个重要组成部分.在性能上,ASP.NET比以往的ASP 3.0更加强大和稳定:ASP.NET提供了更易于编写.结构更清晰的代码,这些代码很容易进行再利用和共享:ASP.NET使用编译后的语言,从而提升性能和伸缩性:ASP.NET使用Web表单使开发更直观,利用面向对象技术促进组件的再利用.另外,ASP.NET中还包括有页面事件.Web控件.缓冲技术以及服务器控件和对数据捆绑的改进

  • 微软TECHED2008 我的讲座:DEV354使用VSTS快速开发高性能WPF复合应用程序的PPT

    微软TECHED2008 我的讲座:DEV354使用VSTS快速开发高性能WPF复合应用程序的PPT 欢迎大家下载 另外12月份等视频处理完了,我会发到网上. 谢谢大家支持 点击下载 [url]http://cid-b10793e754ad25f7.skydrive.live.com/browse.aspx/TechED2008%20China%20%20Dev354%20PPT%20%20%20Duan%20Junyi[/url] powerpoint 2003格式 [url]http://c

  • 编写一个简单的C++程序

    编写一个简单的C++程序 每个C++程序都包含一个或多个函数(function),其中一个必须命名为main.操作系统通过调用main来运行C++程序.下面是一个非常简单的main函数,它什么也不干,只是返回给操作系统一个值: [cpp] view plaincopy int main() { return 0; } 一个函数的定义包含四部分:返回类型(return type).函数名(function name).一个括号包围的形参列表(parameterlist,允许为空)以及函数体(fun

  • 编写更少bug的程序的六条准则

    如何编写更少bug的程序? 尽可能避免常见的程序错误. 沟通设计先行 + 编写可复用代码 + 做得更多 + 做的更少 + 创造"编程心流"+ 严格的程序测试 一. 沟通设计先行 为避免需求或业务逻辑理解的不完全或偏差, 或者设计上存在较大的缺陷, 导致后期不必要的返工, 耗费宝贵的时间和精力, 编程开发遵循的首要准则是沟通设计先行: 与需求方.业务方和工作伙伴多多沟通交流, 进行设计评审, 能够有效地减少因为需求.业务理解偏差导致的逻辑错误和个人经验不足导致的设计错误. 编程与交流活动

  • 编写第一个C语言程序

    前面几节介绍了常量和变量.运算符.表达式和语句的概念,对它们的使用有了一个大概的了解.也许刚学程序的人会觉得有些枯燥,下面我们就来编写第一个C语言程序. #define PI 3.1416 main() { float Radius,Area; scanf(%f,&Radius); /*输入半径的值*/ Area=PI*Radius*Radius; printf(%f\n,Area); /*输出圆的面积*/ } 1.一个C语言程序,通常由带有#号的编译预处理语句开始.关于预处理我们在以后介绍,这

  • 使用Java编写基于命令行的程序

    平时工作中,会用Java编写基于命令行的小工具.本片将介绍如何使用Java编写基于命令行的程序. 命令行程序需要包含的内容 命令行参数解析 程序以及参数说明 程序功能的实现(这个根据自己情况而定) Java中如何处理命令行参数 最直接的处理命令行的方法: public class Cli { public static void main (String [] args) { for(String arg : args ) { System.out.println(arg); } } } 运行:

  • 用Java NIO编写高性能的服务器

    从JDK 1.4开始,Java的标准库中就包含了NIO,即所谓的"New IO".其中最重要的功能就是提供了"非阻塞"的IO,当然包括了Socket.NonBlocking的IO就是对select(Unix平台下)以及WaitForMultipleObjects(Windows平台)的封装,提供了高性能.易伸缩的服务架构. 说来惭愧,直到JDK1.4才有这种功能,但迟到者不一定没有螃蟹吃,NIO就提供了优秀的面向对象的解决方案,可以很方便地编写高性能的服务器. 话说

Tags: