| | | | | | | [文章信息] | | | 作者: | Gregor Noriskin | | 时间: | 2003-08-28 | | 出处: | 微软 | | 责任编辑: | 方舟 | |
| [文章导读] | | | 学习如何找出托管代码性能的最佳方法,以及如何测量托管应用程序的性能...... | |
| |
|
| | | |
|
|
|
|
|
线程和同步
CLR 提供丰富的线程和同步功能,包括创建自己的线程、线程池和各种同步原语的能力。在充分利用 CLR 中支持的线程之前,应该仔细考虑一下线程的用法。请记住,添加线程实际上会降低吞吐量,而不会增加吞吐量,但它肯定会增加内存的利用率。在将要在多处理器计算机上运行的服务器应用程序中,采用并行操作(尽管这取决于将要执行多少锁争用,例如,序列化执行方式)来添加线程可以显著地提高吞吐量;在客户端应用程序中,添加显示活动和/或进度的线程可以提高反应性能(低吞吐量开销)。
如果应用程序中的线程不是专门用于特定任务的线程,或者关联有特殊的状态,则应该考虑使用线程池。如果您过去使用过 Win32 线程池,那对 CLR 线程池也一定会比较熟悉。每个托管进程仅存在一个线程池实例。线程池可以智能地识别出它所创建的线程数量,并且会根据计算机上的负载对自身进行调节。
要讨论线程处理,就必须讨论同步。所有由多线程为应用程序带来的吞吐量收益都可能会因为同步逻辑编写不正确而全部丧失。锁定的粒度会大大影响应用程序的总体吞吐量,这是因为创建和管理锁会带来系统开销,并且锁定很可能会序列化执行步骤所致。我将使用在树中添加节点的示例来说明这一观点。例如,如果树将成为共享的数据结构,则多线程需要在执行应用程序期间对其进行访问,而且您需要对该树进行同步访问。您可以选择在添加节点的同时锁定整个树,这意味着您只会在单一锁定时带来开销,但其他试图访问该树的线程都可能会因此而阻塞。这将是一个粗粒度的锁定示例。或者,您可以在遍历该树时锁定每个节点,这意味着您会在每个节点上创建锁时带来开销,但是其他线程不会因此而阻塞,除非它们试图访问您已经锁定的特定节点。这是细粒度的锁定示例。仅为要对其进行操作的子树添加锁也许是更合适的锁定粒度。请注意,在本示例中,您可能会使用共享锁 (RWLock),因为只有这样才能让多个读者同时进行访问。
执行同步操作时最简单有效的方法就是使用 System.Threading.Interlocked 类。Interlocked 类提供大量低级别的原子操作:Increment、Decrement、Exchange 和 CompareExchange。
在 C# 中使用 System.Threading.Interlocked 类
using System.Threading; //... public class MyClass { void MyClass() //构造函数 { //以原子方式递增全局实例计数器的值 Interlocked.Increment(ref MyClassInstanceCounter); }
~MyClass() //终结器 { //以原子方式递减全局实例计数器的值 Interlocked.Decrement(ref MyClassInstanceCounter); //... } //... } | 最常用的同步机制可能是监测器 (Monitor) 或临界区 (Critical Section)。监测器锁可直接使用,也可以借助 C# 中的 lock 关键字来使用。对于给定的对象来说,lock 关键字会对特定的代码块进行同步访问。从性能的角度来说,如果监测器锁的争用率较低,则系统开销相对较小;但是如果其争用率较高,系统开销也会相对较大。
C# lock 关键字
//线程试图获取 锁 //和代码块 lock(mySharedObject) { //如果块中包含锁, //线程只能执行此块中的代码 }//线程释放锁 | RWLock 提供的是共享锁定机制:例如,该锁可以在“读者”之间共享,但是不能在“作者”之间共享。在这种锁也适用的情况下,使用 RWLock 可以比使用监测器带来更好的吞吐量,它每次只允许一位读者或作者获得该锁。System.Threading 命名空间也包括 Mutex 类。Mutex 是一种同步原语,可用来进行跨进程的同步操作。请注意,它比临界区的开销要大很多,仅当需要进行跨进程的同步操作时才应使用它。
反射
反射是由 CLR 提供的一种机制,用于在运行时通过编程方式获得类型信息。反射在很大程度上取决于嵌入在托管程序集中的元数据。许多反射 API 都要求搜索并分析元数据,这些操作的开销都很大。
这些反射 API 可以分为三个性能区间:类型比较、成员枚举和成员调用。这些区间的系统开销一直在变大。类型比较操作,例如 C# 中的 typeof、GetType、is、IsInstanceOfType 等,都是开销最小的反射 API,尽管它们的实际开销一点也不小。成员枚举操作可以通过编程方式对类的方法、属性、字段、事件、构造函数等进行检查。例如,可能会在设计时的方案中使用这一类的成员枚举操作,在这种情况下,此操作将枚举 Visual Studio 中的 Property Browser(属性浏览器)的 Customs Web Controls(自定义 Web 控件)的属性。那些用于动态调用类成员或动态发出 JIT 并执行某个方法的的反射 API 是开销最大的反射 API。当然,如果需要动态加载程序集、类型实例化以及方法调用,还存在一种后期绑定方案,但是这种松散的耦合关系需要进行明确的性能权衡。一般情况下,应该在对性能影响很大的代码路径中避免使用反射 API。请注意,尽管您没有直接使用反射,但是您使用的 API 可能会使用它。因此,也要注意是否间接使用了反射 API。
后期绑定
后期绑定调用是一种在内部使用反射的功能。Visual Basic.NET 和 JScript.NET 都支持后期绑定调用。例如,使用变量之前您不必进行声明。后期绑定对象实际上是类型对象,可以在运行时使用反射将该对象转换为正确的类型。后期绑定调用比直接调用要慢几个数量级。除非您确实需要后期绑定行为,否则应该避免在性能关键代码路径中使用它。
提示:如果您正在使用 VB.NET,且并不一定需要后期绑定,您可以在源文件的顶部包含 Option Explicit On 和 Option Strict On 以便通知编译器拒绝后期绑定。这些选项将强制您进行声明,并要求您设置变量类型并关闭隐式转换。
安全性
安全性是必要的而且也是主要的 CLR 的组成部分,使用它时会降低性能。当代码为 Fully Trusted(完全信任)且安全策略为默认设置时,安全性对应用程序的吞吐量和启动时间的影响会很小。对代码持不完全信任态度(例如,来自 Internet 或 Intranet 区域的代码)或缩小 MyComputer Grant Set 都将增加安全性的性能开销。
COM 互操作和平台调用
COM 互操作和平台调用会以几乎透明的方式为托管代码提供本机 API,通常调用大多数本机 API 时都不需要任何特殊代码,但是可能需要使用鼠标进行多次单击。正如您所预计的,从托管代码中调用本机代码会带来开销,反之亦然。这笔开销由两部分组成:一部分是固定开销,此开销与在本机代码和托管代码之间进行的转换有关;另一部分是可变开销,此开销与那些可能要用到的参数封送和返回值有关。COM 互操作和平台调用的固定开销在开销中占的比例较小:通常不超过 50 条指令。在各托管类型之间进行封送处理的开销取决于它们在边界两侧的表示形式的相似程度。需要进行大量转换的类型开销相对较大。例如,CLR 中的所有字符串都为 Unicode 字符串。如果要通过平台调用需要 ANSI 字符数组的 Win32 API,则必须缩小该字符串中的每个字符。但是,如果是将托管的整数数组传递到需要本机整数数组的类型中时,就不需要进行封送处理。
由于存在与调用本机代码相关的性能开销,因此您应该确保该开销是合理的开销。如果您打算进行本机调用,请确保本机调用所做的工作使得因执行此调用而产生的性能开销划算,即尽量使方法“小而精”而非“大而全”。测量本机调用开销的一种好方法是测量不接受任何参数也不具备任何返回值的本机方法的性能,然后再测量您希望调用的本机方法的性能。它们之间的差异即封送处理的开销。
提示:应创建“小而精”的 COM 互操作和平台调用,而不是“大而全”的调用,并确保调用的开销对于调用的工作量是划算的。
请注意,不存在与托管线程相关的线程模式。当您打算进行 COM 互操作调用时,需要确保已将执行调用的线程初始化为正确的 COM 线程模式。此操作通常是使用 MTAThreadAttribute 和 STAThreadAttribute 来实现的(尽管也可以通过编程来实现)。
性能计数器
有大量的 Windows 性能计数器可供 .NET CLR 使用。当开发人员首次诊断性能问题,或试图识别托管应用程序的性能特点时,这些性能计数器就是他们可以选择的武器。我已经简要介绍了几个与内存管理和异常有关的性能计数器。在 CLR 和 .NET Framework 中,性能计数器几乎无处不在。通常,这些性能计数器都可以使用且对系统无害,它们的开销较低,而且不会改变应用程序的性能特征。
其他工具
除了性能计数器和 CLR 分析器以外,您还需要使用常规的分析器来确定应用程序中的哪些方法花费的时间最多,且最常被调用。这些方法将是您需要最先进行优化的方法。有多种支持托管代码的商用分析器可供使用,包括 Compuware 的 DevPartner Studio Professional Edition 7.0 和 Intel? 的 VTune? Performance Analyzer 7.0。Compuware 还生产一种免费的托管代码分析器,名为 DevPartner Profiler Community Edition。
小结
本文只是从性能的角度初步介绍了 CLR 和 .NET Framework。在 CLR 和 .NET Framework 中,还有许多别的方面也会对应用程序的性能产生影响。我最希望告诉各位开发人员的是:请不要对您的应用程序的目标平台以及您正在使用的 API 的性能做任何的假设。请测量它们!
|
|
|
|
|
|
|
|