您的位置:首页 > 其它

盘点.NET的性能优化策略

2010-06-08 10:18 232 查看
作者:王涛 日期:2008-2-22

本文将介绍以下内容:

— .NET性能优化的策略探讨

— 多种性能优化分析

引言

性能是系统设计的重要因素,然而影响系统性能的要素又是多种多样,例如硬件环境、数据库设计以及软件设计等等。本文将关注集中在.NET中最常见的性能杀手,并以条款的方式来一一展现,某些可能是规则,某些可能是习惯,而某些可能是语法。

本文在分析了.NET自动内存管理机制的基础上,来总结.NET开发中值得关注的性能策略,并以这些策略作为选择的依据和平衡的杠杆。同时,本文的优化条款主要针对.NET基础展开,而不针对专门的应用环节,例如网站性能优化、数据库优化等。

孰优孰劣,比较应用中自有体现。

性能条款

Item1:推荐以Dispose模式来代替Finalize方式。

关于非托管资源的清理,主要有终止化操作和Dispose模式两种,其中Finalize方式存在执行时间不确定,运行顺序不确定,同时对垃圾回收的性能有极大的损伤。因此强烈建议以Dispose模式来代替Finalize方式,在带来性能提升的同时,实现了更加灵活的控制权。

Item2:在适当的情况下对对象实现弱引用。

为对象实现弱引用,是有效提高性能的手段之一。弱引用是对象引用的一种“中间态”,实现了对象既可以通过GC回收其内存,又可被应用程序访问的机制。这种看似矛盾的解释,的确对胖对象的内存性能带来提升,因为胖对象需要大量的内存来创建,弱引用机制保证了胖对象在内存不足时GC可以回收,而不影响内存使用,在没有被GC回收前又可以再次引用该对象,从而达到空间与时间的双重节约。

在.NET中,WeakReference类用于表示弱引用,通过其Target属性来表示要追踪的对象,通过其值赋给变量来创建目标对象的强引用,例如:

public void WeakRef()
{
MyClass mc = new MyClass();

//创建短弱引用
WeakReference wr = new WeakReference(mc);
//移除强引用
mc = null;

if (wr.IsAlive)
{
//弱引用转换为强引用,对象可以再次使用
mc = wr.Target as MyClass;
}
else
{
//对象已经被回收,重新创建
mc = new MyClass();
}
}

Item3:推荐使用泛型集合来代替非泛型集合。

泛型实现了一种类型安全的算法重用,其最直接的应用正是在集合类中的性能与安全的良好体现,因此我们建议以泛型集合来代替非泛型集合,以List<T>和ArrayList为例来做以说明:

public static void Main()
{
//List<T>性能测试
List<Int32> list = new List<Int32>();
for (Int32 i = 0; i < 10000; i++)
//未发生装箱
list.Add(i);

//ArrayList性能测试
ArrayList al = new ArrayList();
for (Int32 j = 0; j < 10000; j++)
//发生装箱
al.Add(j);
}

上述示例,仅仅给出了泛型集合和非泛型集合在装箱操作上引起的差别,同样的拆箱操作也伴随了这两种不同集合的取值操作。同时,大量的装箱操作会带来频繁的垃圾回收,类型转换时的安全检查,都不同程度的影响着性能,而这些弊端在泛型集合中荡然无存。

必须明确的是,泛型集合并不能完全代替非泛型集合的应用,.NET框架类库中有大量的集合类用以完成不同的集合操作,例如ArrayList中包含的很多静态方法是List<T>所没有的,而这些方法又能为集合操作带来许多便利。因此,恰当地做出选择是非常重要的。

注意,这种性能差别对值类型的影响较大,而引用类型不存在装箱与拆箱问题,因此性能影响不是很明显。

Item4:初始化时最好为集合对象指定大小。

长度动态增加的集合类,例如ArrayList、Queue的等。可以无需指定其容量,集合本身能够根据需求自动增加集合大小,为程序设计带来方便。然而,过分依赖这种特性并非好的选择,因为集合动态增加的过程是一个内存重新分配和集合元素复制的过程,对性能造成一定的影响,所以有必要在集合初始化时指定一个适当的容量。例如:

public static void Main()
{
ArrayList al = new ArrayList(2);
al.Add("One");
al.Add("Two");
//容量动态增加一倍
al.Add("Three");

Console.WriteLine(al.Capacity);
}

Item5:特定类型的Array性能优于ArrayList。

ArrayList只接受Object类型的元素,向ArrayList添加其他值类型元素会发生装箱与拆箱操作,因此在性能上使用Array更具优势,当然object类型的数组除外。不过,ArrayList更容易操作和使用,所以这种选择同样存在权衡与比较。

Item6:字符串驻留机制,是CLR为String类型实现的特殊设计。

String类型无疑是程序设计中使用最频繁、应用最广泛的基元类型,因此CLR在设计上为了提升String类型性能考虑,实现了一种称为“字符串驻留”的机制,从而实现了相同字符串可能共享内存空间。同时,字符串驻留是进程级的,垃圾回收不能释放CLR内部哈希表维护的字符串对象,只有进程结束时才释放。这些机制均为String类型的性能提升和内存优化提供了良好的基础。

Item7:合理使用System.String和System.Text.StringBuilder。

在简单的字符串操作中使用String,在复杂的字符串操作中使用StringBuilder。简单的说,StringBuilder对象的创建代价较大,在字符串连接目标较少的情况下,应优先使用String类型;而在有大量字符串连接操作的情况下,应优先考虑StringBuilder。

同时,StringBuilder在使用上,最好指定合适的容量值,否则由于默认容量的不足而频繁进行内存分配的操作会影响系统性能。

Item8:尽量在子类中重写ToString方法。

ToString方法是System.Object提供的一个公有的虚方法,.NET中任何类型都可继承System.Object类型提供的实现方法,默认为返回类型全路径名称。在自定义类或结构中重写ToString方法,除了可以有效控制输出结果,还能在一定程度上减少装箱操作的发生。

public struct User
{
public string Name;
public Int32 Age;

//避免方法调用时的装箱
public override string ToString()
{
return "Name: " + Name + ", Age:" + Age.ToString();
}
}

Item9:其他推荐的字符串操作。

字符串比较,常常习惯的做法是:

public bool StringCompare(string str1, string str2)
{
return str1 == str2;
}

而较好的实现应该是:

public int StringCompare(string str1, string str2)
{
return String.Compare(str1, str2);
}

二者的差别是:前者调用String.Equals方法操作,而后者调用String. Compare方法来实现。String.Equals方法实质是在内部调用一个EqualsHelper辅助方法来实施比较,内部处理相对复杂。因此,建议使用String.Compare方式进行比较,尤其是非大小写敏感字符串的比较,在性能上更加有效。

类似的操作包含字符串判空的操作,推荐的用法以Length属性来判断,例如:

public bool IsEmpty(string str)
{
return str.Length == 0;
}

Item10:for和foreach的选择。

推荐选择foreach来处理可枚举集合的循环结构,原因如下:

.NET 2.0以后编译器对foreach进行了很大程度的改善,在性能上foreach和for实际差别不大。

foreach语句能够迭代多维数组,能够自动检测数组的上下限。

foreach语句能够自动适应不同的类型转换。

foreach语句代码更简洁、优雅,可读性更强。

public static void Main()
{
ArrayList al = new ArrayList(3);
al.Add(100);
al.Add("Hello, world.");
al.Add(new char[] { 'A', 'B', 'C' });

foreach (object o in al)
Console.WriteLine(o.ToString());

for (Int32 i = 0; i < al.Count; i++)
Console.WriteLine(al[i].ToString());
}


Item11:以多线程处理应对系统设计。

毫无疑问,多线程技术是轻松应对多任务处理的最强大技术,一方面能够适应用户的响应,一方面能在后台完成相应的数据处理,这是典型的多线程应用。在.NET中,基于托管环境的多个线程可以在一个或多个应用程序域中运行,而应用多个线程来处理不同的任务也造成一定的线程同步问题,同时过多的线程有时因为占用大量的处理器时间而影响性能。

推荐在多线程编程中使用线程池,.NET提供了System.Threading.ThreadPool类来提供对线程池的封装,一个进程对应一个ThreadPool,可以被多个AppDomain共享,能够完成异步I/O操作、发送工作项、处理计时器等操作,.NET内部很多异步方法都使用ThreadPool来完成。在此做以简单的演示:

class ThreadHandle
{
public static void Main()
{
ThreadHandle th = new ThreadHandle();

//将方法排入线程池队列执行
ThreadPool.QueueUserWorkItem(new WaitCallback(th.MyProcOne), "线程1");
Thread.Sleep(1000);
ThreadPool.QueueUserWorkItem(new WaitCallback(th.MyProcTwo), "线程2");

//实现阻塞主线程
Console.Read();
}

//在不同的线程执行不同的回调操作
public void MyProcOne(object stateInfo)
{
Console.WriteLine(stateInfo.ToString());
Console.WriteLine("起床了。");
}

public void MyProcTwo(object stateInfo)
{
Console.WriteLine(stateInfo.ToString());
Console.WriteLine("刷牙了。");
}
}

然而,多线程编程将使代码控制相对复杂化,不当的线程同步可能造成对共享资源的访问冲突等待,在实际的应用中应该引起足够的重视。

Item12:尽可能少地抛出异常,禁止将异常处理放在循环内。

异常的发生必然造成系统流程的中断,同时过多的异常处理也会对性能造成影响,应该尽量用逻辑流程控制来代替异常处理。对于例行发生的事件,可以通过编程检查方式来判断其情况,而不是一并交给异常处理,例如:

Console.WriteLine(obj == null ? String.Empty : obj.ToString());

不仅简洁,而且性能表现更好,优于以异常方式的处理:

try
{
Console.WriteLine(obj.ToString());
}
catch (NullReferenceException ex)
{
Console.WriteLine(ex.Message);
}

当然,大部分情况下以异常机制来解决异常信息是值得肯定的,能够保证系统安全稳定的面对不可意料的错误问题。例如不可预计的溢出操作、索引越界、访问已关闭资源等操作,则应以异常机制来处理。

Item13:捕获异常时,catch块中尽量指定具体的异常筛选器,多个catch块应该保证异常由特殊到一般的排列顺序。

指定具体的异常,可以节约CLR搜索异常的时间;而CLR是按照自上而下的顺序搜索异常,因此将特定程度较高的排在前面,而将特定程度较低的排在后面,否则将导致编译错误。

Item14:struct和class的性能比较。

基于性能的考虑,在特殊情况下,以struct来实现对轻量数据的封装是较好的选择。这是因为,struct是值类型,数据分配于线程的堆栈上,因此具有较好的性能表现。对值类型对象和引用类型对象的分配进行了越深入的理解,就能更好的关注在线程栈上进行内存分配具有较高的执行效率。

当然,绝大部分情况下,class都具有不可代替的地位,在面向对象程序世界里更是如此。

Item15:以is/as模式进行类型兼容性检查。

以is和as操作符可以用于判断对象类型的兼容性,以is来实现类型判断,以as实现安全的类型转换,是值得推荐的方法。这样能够避免不必要的异常抛出,从而实现一种安全、灵活的转换控制。例如:

public static void Main()
{
MyClass mc = new MyClass();
if (mc is MyClass)
{
Console.WriteLine("mc is a MyClass object.");
}

object o = new object();
MyClass mc2 = o as MyClass;

if (mc2 != null)
{
//对转换类型对象执行操作
}
}

Item16:const和static readonly的权衡。

const是编译时常量,readonly是运行时常量,所以const高效,readonly灵活。在实际的应用中,推荐以static readonly来代替const,以解决const可能引起的程序集引用不一致问题,还有带来的较多灵活性控制。

Item17:尽量避免不当的装箱和拆箱,选择合适的代替方案。

通过本文多个条款的性能讨论,我们不难发现很多情况下影响性能的正是装箱和拆箱,例如非泛型集合操作,类型转换等,因此选择合适的替代方案是很有必要的。可以使用泛型集合来代替非泛型集合,可以实现多个重载方法以接受不同类型的参数来减少装箱,可以在子类中重写ToString方法来避免装箱等等。

Item18:尽量使用一维零基数组。

CLR对一维零基数组使用了特殊的IL操作指令newarr,在访问数组时不需要通过索引减去偏移量来完成,而且JIT也只需执行一次范围检查,可以大大提升访问性能。在各种数组中其性能最好、访问效率最高,因此值得推荐。

结论

性能条款就是系统开发过程中的杠杆,在平衡功能与性能之间做出恰当的选择,本文的18条选择策略仅从最普遍意义的选择角度进行了分析,这些条款应该作为开发人员软件设计的参照坐标,并应用于实际的代码编写中。

通读所有条款,你可能会发现本文在一定程度上对.NET在语言层面进行了一次全面的梳理,个中条款以简单的方式呈现,渗透了大师们对于.NET开发的智慧和经验,作者有幸作为一个归纳梳理的后辈,从中受益匪浅。

参考文献

(USA)Richard Jones, Rafael D Lins, Garbage Collection: Algorithms for Automatic Dynamic Memory Management

(MSDN)Emmanuel Schanzer, Performance Considerations for Run-Time Technologies in the .NET Framework, http://msdn2.microsoft.com/en-us/library/ms973838.aspx
(MSDN) Gregor Noriskin,Tips and Tricks for Performance in .NET Applications, http://msdn2.microsoft.com/en-us/library/ms973839.aspx
注:本文部分内容节选自作者技术专著《你必须知道的.NET》。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: