`
bluedusk
  • 浏览: 270195 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

C# 性能优化方面的总结

    博客分类:
  • .Net
阅读更多

垃圾回收

垃圾回收解放了手工管理对象的工作,提高了程序的健壮性,但副作用就是程序代码可能对于对象创建变得随意。

1.1  

由于垃圾回收的代价较高,所以C#程序开发要遵循的一个基本原则就是避免不必要的对象创建。以下列举一些常见的情形。

1.1.1 避免循环创建对象 ★

如果对象并不会随每次循环而改变状态,那么在循环中反复创建对象将带来性能损耗。高效的做法是将对象提到循环外面创建。

1.1.2 在需要逻辑分支中创建对象

如果对象只在某些逻辑分支中才被用到,那么应只在该逻辑分支中创建对象。

1.1.3 使用常量避免创建对象

程序中不应出现如 new Decimal(0) 之类的代码,这会导致小对象频繁创建及回收,正确的做法是使用Decimal.Zero常量。我们有设计自己的类时,也可以学习这个设计手法,应用到类似的场景中。

1.2

如果类包含析构函数,由创建对象时会在 Finalize 队列中添加对象的引用,以保证当对象无法可达时,仍然可以调用到 Finalize 方法。垃圾回收器在运行期间,会启动一个低优先级的线程处理该队列。相比之下,没有析构函数的对象就没有这些消耗。如果析构函数为空,这个消耗就毫无意义,只会导致性能降低!因此,不要使用空的析构函数。

在实际情况中,许多曾在析构函数中包含处理代码,但后来因为种种原因被注释掉或者删除掉了,只留下一个空壳,此时应注意把析构函数本身注释掉或删除掉。

1.3

垃圾回收事实上只支持托管内在的回收,对于其他的非托管资源,例如 Window GDI 句柄或数据库连接,在析构函数中释放这些资源有很大问题。原因是垃圾回收依赖于内在紧张的情况,虽然数据库连接可能已濒临耗尽,但如果内存还很充足的话,垃圾回收是不会运行的。

C#

为防止对象的 Dispose 方法不被调用的情况发生,一般还要提供析构函数,两者调用一个处理资源释放的公共方法。同时,Dispose 方法应调用 System.GC.SuppressFinalize(this),告诉垃圾回收器无需再处理 Finalize 方法了。

2 String 操作

2.1

String是不变类,使用 + 操作连接字符串将会导致创建一个新的字符串。如果字符串连接次数不是固定的,例如在一个循环中,则应该使用 StringBuilder 类来做字符串连接工作。因为 StringBuilder 内部有一个 StringBuffer ,连接操作不会每次分配新的字符串空间。只有当连接后的字符串超出 Buffer 大小时,才会申请新的 Buffer 空间。典型代码如下:

StringBuilder sb = new StringBuilder(256 );

for ( int i = 0 ; i < Results.Count; i ++ )

{

sb.Append (Results[i]);

} 

如果连接次数是固定的并且只有几次,此时应该直接用 + 号连接,保持程序简洁易读。实际上,编译器已经做了优化,会依据加号次数调用不同参数个数的 String.Concat 方法。例如:String str = str1+ str2 + str3 + str4;

会被编译为 String.Concat(str1, str2, str3, str4)。该方法内部会计算总的 String 长度,仅分配一次,并不会如通常想象的那样分配三次。作为一个经验值,当字符串连接操作达到 10 次以上时,则应该使用StringBuilder

这里有一个细节应注意:StringBuilder 内部 Buffer 的缺省值为 16 ,这个值实在太小。按 StringBuilder 的使用场景,Buffer 肯定得重新分配。经验值一般用 256 作为 Buffer 的初值。当然,如果能计算出最终生成字符串长度的话,则应该按这个值来设定Buffer 的初值。使用 new StringBuilder(256)就将 Buffer 的初始长度设为了256。

2.2

String

例如,bool.Parse方法本身已经是忽略大小写的,调用时不要调用ToLower方法。

另一个非常普遍的场景是字符串比较。高效的做法是使用 Compare 方法,这个方法可以做大小写忽略的比较,并且不会创建新字符串。

还有一种情况是使用 HashTable 的时候,有时候无法保证传递 key 的大小写是否符合预期,往往会把 key 强制转换到大写或小写方法。实际上 HashTable 有不同的构造形式,完全支持采用忽略大小写的 key: new HashTable(StringComparer.OrdinalIgnoreCase)。

2.3

将String对象的Length属性与0比较是最快的方法:if (str.Length == 0)其次是与String.Empty常量或空串比较:if (str == String.Empty)或if (str == "")

 注:C#在编译时会将程序集中声明的所有字符串常量放到保留池中(intern pool),相同常量不会重复分配。

3

3.1

线程同步是编写多线程程序需要首先考虑问题。C#为同步提供了 MonitorMutexAutoResetEvent 和 ManualResetEvent对象来分别包装 Win32 的临界区、互斥对象和事件对象这几种基础的同步机制。C#还提供了一个lock语句,方便使用,编译器会自动生成适当的 Monitor.Enter 和 Monitor.Exit 调用。

3.1.1 同步粒度

同步粒度可以是整个方法,也可以是方法中某一段代码。为方法指定 MethodImplOptions.Synchronized 属性将标记对整个方法同步。例如:

[MethodImpl(MethodImplOptions.Synchronized)]

public static SerialManagerGetInstance()

{

if (instance == null )

{

instance = new SerialManager();

}

return instance;

}

通常情况下,应减小同步的范围,使系统获得更好的性能。简单将整个方法标记为同步不是一个好主意,除非能确定方法中的每个代码都需要受同步保护。

3.1.2 同步策略

使用 lock 进行同步,同步对象可以选择 Type、this 或为同步目的专门构造的成员变量。

避免锁定Type

锁定Type对象会影响同一进程中所有AppDomain该类型的所有实例,这不仅可能导致严重的性能问题,还可能导致一些无法预期的行为。这是一个很不好的习惯。即便对于一个只包含static方法的类型,也应额外构造一个static的成员变量,让此成员变量作为锁定对象。

避免锁定 this

锁定 this 会影响该实例的所有方法。假设对象 obj A  B 两个方法,其中 A 方法使用 lock(this) 对方法中的某段代码设置同步保护。现在,因为某种原因,B 方法也开始使用 lock(this) 来设置同步保护了,并且可能为了完全不同的目的。这样,A 方法就被干扰了,其行为可能无法预知。所以,作为一种良好的习惯,建议避免使用lock(this) 这种方式。

使用为同步目的专门构造的成员变量

这是推荐的做法。方式就是 new 一个 object 对象, 该对象仅仅用于同步目的。

如果有多个方法都需要同步,并且有不同的目的,那么就可以为些分别建立几个同步成员变量。

3.1.4 集合同步

C#

// Creates andinitializes a new ArrayList

ArrayList myAL = new ArrayList();

myAL.Add( " The " );

myAL.Add( " quick " );

myAL.Add( " brown " );

myAL.Add( " fox " );

// Creates asynchronized wrapper around the ArrayList

ArrayList mySyncdAL = ArrayList.Synchronized(myAL);

调用 Synchronized 方法会返回一个可保证所有操作都是线程安全的相同集合对象。考虑mySyncdAL[0] = mySyncdAL[0]+ "test" 这一语句,读和写一共要用到两个锁。一般讲,效率不高。推荐使用 SyncRoot 属性,可以做比较精细的控制。

3.2

存取 NameDataSlot  Thread.GetData 和 Thread.SetData 方法需要线程同步,涉及两个锁:一个是 LocalDataStore.SetData 方法需要在 AppDomain 一级加锁,另一个是 ThreadNative.GetDomainLocalStore 方法需要在 Process 一级加锁。如果一些底层的基础服务使用了 NameDataSlot,将导致系统出现严重的伸缩性问题。

规避这个问题的方法是使用 ThreadStatic 变量。示例如下:

public sealed class InvokeContext

{

[ThreadStatic]

private static InvokeContext current;

private Hashtable maps = new Hashtable();

} 

 

3.3

3.3.1 使用Double Check 技术创建对象

internal IDictionary KeyTable

{

get

{

if ( this ._keyTable == null )

{

lock ( base ._lock)

{

if ( this ._keyTable == null )

{

this ._keyTable = new Hashtable();

}

}

}

return this ._keyTable;

}

} 

创建单例对象是很常见的一种编程情况。一般在 lock 语句后就会直接创建对象了,但这不够安全。因为在 lock 锁定对象之前,可能已经有多个线程进入到了第一个 if 语句中。如果不加第二个 if 语句,则单例对象会被重复创建,新的实例替代掉旧的实例。如果单例对象中已有数据不允许被破坏或者别的什么原因,则应考虑使用 DoubleCheck 技术。

4

4.1

CLR

需要注意的是:方法中的局部变量不是从堆而是上分配,所以C#不会做清零工作。如果使用了未赋值的局部变量,编译期间即会报警。不要因为有这个印象而对所有类的成员变量也做赋值动作,两者的机理完全不同!

4.2 ValueType 和 ReferenceType

4.2.1 以引用方式传递值类型参数

值类型从调用分配,引用类型从托管堆分配。当值类型用作方法参数时,默认会进行参数值复制,这抵消了值类型分配效率上的优势。作为一项基本技巧,以引用方式传递值类型参数可以提高性能。

4.2.2  ValueType 提供 Equals 方法

.net

public struct Rectangle

{

public double Length;

public double Breadth;

public override bool Equals ( objectob)

{

if (ob is Rectangle)

return Equels ((Rectangle)ob))

else

return false ;

}

private bool Equals (Rectangle rect)

{

return this .Length == rect.Length&& this .Breadth == rect.Breach;

}

} 

4.2.3 避免装箱和拆箱

C#

一种经常的情形出现在使用集合类型时。例如:

ArrayList al = new ArrayList();

for ( int i = 0 ; i < 1000 ; i ++ )

{

al.Add(i);// Implicitly boxed because Add() takes an object

}

int f = ( int )al[ 0]; // The element is unboxed 

5

异常也是现代语言的典型特征。与传统检查错误码的方式相比,异常是强制性的(不依赖于是否忘记了编写检查错误码的代码)、强类型的、并带有丰富的异常信息(例如调用栈)。

5.1

关于异常处理的最重要原则就是:不要吃掉异常。这个问题与性能无关,但对于编写健壮和易于排错的程序非常重要。这个原则换一种说法,就是不要捕获那些你不能处理的异常。

吃掉异常是极不好的习惯,因为你消除了解决问题的线索。一旦出现错误,定位问题将非常困难。除了这种完全吃掉异常的方式外,只将异常信息写入日志文件但并不做更多处理的做法也同样不妥。

5.2

有些代码虽然抛出了异常,但却把异常信息吃掉了。

为异常披露详尽的信息是程序员的职责所在。如果不能在保留原始异常信息含义的前提下附加更丰富和更人性化的内容,那么让原始的异常信息直接展示也要强得多。千万不要吃掉异常。

5.3

抛出异常和捕获异常属于消耗比较大的操作,在可能的情况下,应通过完善程序逻辑避免抛出不必要不必要的异常。与此相关的一个倾向是利用异常来控制处理逻辑。尽管对于极少数的情况,这可能获得更为优雅的解决方案,但通常而言应该避免。

5.4

如果是为了包装异常的目的(即加入更多信息后包装成新异常),那么是合理的。但是有不少代码,捕获异常没有做任何处理就再次抛出,这将无谓地增加一次捕获异常和抛出异常的消耗,对性能有伤害。

6

反射是一项很基础的技术,它将编译期间的静态绑定转换为延迟到运行期间的动态绑定。在很多场景下(特别是类框架的设计),可以获得灵活易于扩展的架构。但带来的问题是与静态绑定相比,动态绑定会对性能造成较大的伤害。

6.1

typecomparison :类型判断,主要包括 is 和 typeof 两个操作符及对象实例上的 GetType 调用。这是最轻型的消耗,可以无需考虑优化问题。注意 typeof 运算符比对象实例上的 GetType 方法要快,只要可能则优先使用 typeof 运算符。

memberenumeration : 成员枚举,用于访问反射相关的元数据信息,例如Assembly.GetModuleModule.GetTypeType对象上的IsInterfaceIsPublicGetMethodGetMethodsGetPropertyGetPropertiesGetConstructor调用等。尽管元数据都会被CLR缓存,但部分方法的调用消耗仍非常大,不过这类方法调用频度不会很高,所以总体看性能损失程度中等。

memberinvocation:成员调用,包括动态创建对象及动态调用对象方法,主要有Activator.CreateInstanceType.InvokeMember等。

6.2

C#

  1. Type.InvokeMember

  2. ContructorInfo.Invoke

  3. Activator.CreateInstance(Type)

  4. Activator.CreateInstance(assemblyNametypeName)

  5. Assembly.CreateInstance(typeName)

最快的是方式 3 ,与 Direct Create 的差异在一个数量级之内,约慢 7 的水平。其他方式,至少在 40以上,最慢的是方式 4 ,要慢三个数量级。

6.3

方法调用分为编译期的早期绑定和运行期的动态绑定两种,称为Early-Bound InvocationLate-Bound Invocation。Early-Bound Invocation可细分为Direct-call、Interface-call和Delegate-call。Late-Bound Invocation主要有Type.InvokeMemberMethodBase.Invoke,还可以通过使用LCG(Lightweight Code Generation)技术生成IL代码来实现动态调用。

从测试结果看,相比Direct CallType.InvokeMember要接近慢三个数量级;MethodBase.Invoke虽然比Type.InvokeMember要快三倍,但比Direct Call仍慢270倍左右。可见动态方法调用的性能是非常低下的。我们的建议是:除非要满足特定的需求,否则不要使用!

6.4

模式

  1. 如果可能,则避免使用反射和动态绑定

  2. 使用接口调用方式将动态绑定改造为早期绑定

  3. 使用Activator.CreateInstance(Type)方式动态创建对象

  4. 使用typeof操作符代替GetType调用

反模式

  1. 在已获得Type的情况下,却使用Assembly.CreateInstance(type.FullName)

7

这里描述一些应用场景下,可以提高性能的基本代码技巧。对处于关键路径的代码,进行这类的优化还是很有意义的。普通代码可以不做要求,但养成一种好的习惯也是有意义的。

7.1

可以把循环的判断条件用局部变量记录下来。局部变量往往被编译器优化为直接使用寄存器,相对于普通从堆或栈中分配的变量速度快。如果访问的是复杂计算属性的话,提升效果将更明显。for (int i = 0, j = collection.GetIndexOf(item);i < j; i++)

需要说明的是:这种写法对于CLR集合类的Count属性没有意义,原因是编译器已经按这种方式做了特别的优化。

7.2

拼装好之后再删除是很低效的写法。有些方法其循环长度在大部分情况下为1,这种写法的低效就更为明显了:

public static string ToString(MetadataKey entityKey)

{

string str = "" ;

object [] vals = entityKey.values;

for ( int i= 0 ; i < vals.Length; i ++ )

{

str += " , " + vals[i]. st

分享到:
评论

相关推荐

    C#性能优化

    总结,C#性能优化是一个全方位的工作,涉及到数据库交互、服务器配置、多线程管理、内存控制以及代码编写等多个层面。深入理解和掌握这些知识点,可以帮助开发者编写出更加高效、稳定的C#应用程序。通过持续学习和...

    .NET 性能优化方法总结

    ### .NET性能优化方法总结 #### 一、C#语言方面 **1.1 垃圾回收** 垃圾回收机制是.NET平台的一项重要特性,它能够自动管理内存资源,减轻了程序员手动管理对象生命周期的负担。然而,垃圾回收并非免费的操作,其...

    NET性能优化方面的总结

    .NET性能优化是一个重要的主题,尤其是对于那些关注应用程序响应速度、资源消耗和系统稳定性的开发者来说。本文主要聚焦在C#语言层面的优化策略,包括垃圾回收、对象创建、字符串操作和资源管理等方面。 1. 垃圾...

    号称最经典c#总结可提高水平

    1. "一些很酷的.NET技巧.Net技巧.doc":可能包含.NET框架中的一些高级特性和技巧,如.NET框架的内存管理、性能优化、跨平台特性等。 2. "c#常用概念.doc":可能详细解释了C#中的关键概念,如面向对象编程、继承、...

    C#编程知识点总结41-50C#编程知识点总结

    映射函数则涉及指针和内存操作,理解它们可以帮助优化内存访问性能。 4. **结构与类的区别**:C#中的结构(Struct)和类(Class)都是数据封装的方式。类是引用类型,结构是值类型。类支持继承和多态,而结构不支持...

    C# 高性能服务器 - 端口-心跳高性能Socket服务器

    总结来说,C#的高性能Socket服务器设计涉及多线程处理、端口管理、心跳机制、异常处理、缓冲区优化等多个方面。开发者需要根据具体需求,结合C#的特性和.NET框架提供的工具,灵活运用上述技巧,以构建出稳定、高效的...

    winform(c#)73种好看的窗体控件优化,界面样式

    同时,注意性能优化,避免过多的资源消耗导致程序运行缓慢。 总结来说,"winform(c#)73种好看的窗体控件优化,界面样式"为Winform应用的界面美化提供了丰富的选择,开发者可以根据项目需求选择合适的皮肤,提升应用...

    .NET开发性能优化 (英文版)

    通过阅读《Pro .NET Performance》,开发者可以深入理解.NET性能优化的各个方面,从而编写出更高效、更稳定的C#应用程序。这本书不仅适合有经验的.NET开发者,也适合那些希望提升自己应用性能的新手,是一本不可多得...

    asp_net性能优化总结材料.pdf

    ASP.NET性能优化是一个关键的议题,它涉及到代码的效率、资源管理以及应用程序的响应速度。以下是一些关于ASP.NET性能优化的关键知识点: 1. **C#语言方面**: - **垃圾回收(Garbage Collection, GC)**:C#中的...

    C#高性能图片差异对比、差异提取源码

    总结起来,这个项目展示了C#在图像处理领域的强大功能,特别是在高效对比和差异提取方面。通过熟练运用Bitmap类和优化策略,开发者能够实现高速的图片处理算法,满足实时性要求高的应用场景。同时,源代码的公开也为...

    C#高性能服务器;端口-心跳高性能Socket服务器

    总结来说,"C#高性能服务器;端口-心跳高性能Socket服务器"是一个综合性的技术话题,涵盖了网络编程、并发处理、心跳机制等多个方面。C#语言提供了丰富的库和工具,使开发者能够构建出稳定、高效的网络服务。在实践...

    精华志 中兴OMC优化分析总结 优化必看

    《精华志 中兴OMC优化分析总结》是针对中兴通信网络管理系统的性能优化进行的一份深入探讨。本文将从标题、描述以及标签所涉及的关键技术出发,详细阐述相关知识点。 首先,我们要理解中兴OMC(Operations and ...

    C#源码 访问ACCESS数据库 通过反射调用命令 清理数据库 执行数据库优化 压缩操作

    总结,本压缩包文件中的C#源码示例展示了如何利用反射技术在运行时灵活地执行对Access数据库的各种管理操作,如清理、优化和压缩。这对于开发者来说是一个有价值的参考资料,可以帮助他们更好地理解和实践C#与数据库...

    C#与C++进程间通信

    总结来说,“C#与C++进程间通信”这个主题涉及了命名管道的原理、创建、连接、数据传输、类型兼容性处理以及性能优化等多个方面。掌握这些知识点对于开发跨语言的多进程应用程序至关重要。通过实践和理解提供的`...

    C#小知识点总结及常见问题

    在C#编程中,开发者经常会遇到各种小知识点和常见问题,尤其是在处理页面交互、数据转换、文件操作以及数据展示方面。...在实际项目中,开发者还应关注错误处理、性能优化以及安全实践等其他关键点。

Global site tag (gtag.js) - Google Analytics