Performance Tips I

假如所有的人都很在意高性能代码的话,那么我的这篇Blog就没有意义了。我希望能够给出一些有意义的性能提示,这些是我们日常编码工作中总会碰到的,所以对这些小tips漠不关心只会日益加深对你代码的伤害,而且做Review的时候也会很不体面,老实说,一个人写的代码确切地反映了这个人的能力与个性,对于经常看不同代码的我们来说,这并不是胡说的,我们有确切的体会。

首先值得一提的是,如果你的程序有性能问题,请不要首先在代码中找,因为一般情况下,大的性能问题都发生在架构与设计上,架构与设计的性能问题是在代码层次解决不了的,影响面也很广。这篇文章不会告诉你如何在架构与设计中找性能问题,或者架构与设计的性能提示。这篇文章只是在编码层次上给这个层次的性能提示,虽然比不上架构与设计的性能优化,但是如果不注意也还是会带来显著的性能影响的。

我会以总结的方式列出每条我能想到的性能提示来,并解析为什么会这样,希望对读者有用。

Resource Management(资源管理)

作为任何一个程序,都需要考虑它所能使用的资源界限以及它目前使用的资源状况。假设一个程序只被允许使用100MB内存,但是它在执行某个大任务时内存占用却达到了400MB,那么这时你就会发现你有个不得不解决的大问题。任何硬件的资源都是有限的(不要跟我说SETI,没有几个程序是SETI类型的),作为程序,如何利用好这些有限的资源达到目的是任何一个开发人员不得不考虑的(不管你是架构师、设计师还是纯粹的Developer)。首先我们要知道所谓的资源其实可以分为几大类:
  1. CPU
  2. Memory
  3. Disk I/O
  4. Network I/O

为了有效的利用这些有限的资源,我们有时不得不针对资源管理做一些优化,下面的这些便是一些相关的tips。可能你会问:“资源的管理.NET下不是自动的嘛,不是有GC的嘛”,没错,GC是做资源管理的,但是这并不代表有GC我们就不用注意资源的使用了,因为GC的行为是固定的,所以我们可以针对GC的行为来为它优化。下面会有几条tips谈到。

只在你真正需要时分配内存。

为什么:

内存的过早分配会造成不必要的效率损失以及资源紧缺或者浪费。

不好的做法:

static Result Divide(double a, double b) {
   
// Result对象过早的分配,当a/b发生异常时
   
// 这个对象就白分配了,因为这个对象只有被
   
// 方法返回才有意义。
   Result r = new Result();
   r.Value 
= a / b;

   
return r;
}

好的做法:

static Result Divide(double a, double b) {
   
double value = a / b;

   Result r 
= new Result();
   r.Value 
= value;

   
return r;
}

尽可能早的释放资源。

为什么:

对于你已经确定不再需要的对象如果不尽早对其释放就有可能造成不必要的资源紧缺。

不好的做法:

static void Foo() {
   
connection.Open();
   
try {
      
// Data operations

      
// PostProcessing并不涉及数据库连接的使用,
      
// 在处理PostProcessing的时候,一个宝贵的
      
// 数据库连接资源就被这样白白的浪费了,如果
      
// 这时有其他需要数据库连接的操作而它又
      
// 没有可用连接那么就会引发timeout等等因数据库
      
// 连接不足而产生的各种问题。
      PostProcessing();
   }
 finally {
      
connection.Close();
   }

}

好的做法:

static void Foo() {
   
connection.Open();

   
try {
      
// Data operations
   }
 finally {
      
connection.Close();
   }


   
PostProcessing();
}

不要使用Finalizer,除非是为了确保关键资源的正确释放。

为什么:

有Finalizer的对象需要两次GC才能被彻底回收,影响内存回收效率,第一次GC会执行Finalizer,第二次才会释放对象所占用的内存空间。

不好的做法:

~MyClass() {
   
// 影响资源回收的速度。
   instanceNumber--;
}

好的做法:

不声明Finalizer。

对于拥有非托管资源的对象,一定要实现Disposable模式。

为什么:

实现了整个Disposable模式才能确保非托管资源会被正确释放(非托管资源不受GC管理,需手动释放),并且提供了手动释放的方法。

不好的做法:

// 拥有文件系统这种非托管资源,但是没有
// 定义关闭这个非托管资源的方法。
class TextFile {
   
private FileStream fileStream;
   
   
public string ReadLine() {
      
   }

   
   
}

好的做法:

class TextFile : IDisposable {
   
private FileStream fileStream;
   
   
public string ReadLine() {
      
   }

   
   
   
   
public void Dispose() {
      
this.Dispose(true);
   }

   
   
protected virtual void Dispose(bool disposing) {
      
if (disposing) {
         
// 在这儿清理一切托管的与Disposable的托管
         
// 成员。
         if (this.fileStream != null{
            
this.fileStream.Close();
            
this.fileStream = null;
         }

         
         
// 因为我们已经显示地清理了资源,所以不再需要
         
// Finalizer了,有它会影响效率。
         GC.SupressFinalize();
      }

      
      
// 在这儿清理一切非托管资源。
      
      
// 如果有基类,应在这里调用基类的Dispose。这样做
      
// 是因为清理顺序应该是首先清理自身的一切资源,
      
// 然后基类清理自身的一切资源,依此类推。
   }

   
   
~TextFile() {
      
this.Dispose(false);
   }

}

在Dispose方法中调用GC.SupressFinalize。

为什么:

就像前面提到的,有Finalizer会影响回收效率,既然你都已经手动Dispose了,那么干吗还要Finalizer呢(注意Finalizer的作用应该限制在调用Dispose上)?

不好的做法:

请参照上一示例。

好的做法:

请参照上一示例。

避免延长存活期短的对象的寿命。

为什么:

存活期短的对象会被GC优先回收,回收的效率更高。如果无谓的延长它的寿命的话再次回收它时效率会更低,而且它也会被更慢的回收。GC回收时是按照generation来回收,新创建的对象都在generation 0中,GC回收时先回收generation 0中的对象,如果回收到足够的空间那么将不再继续回收,如果不够再在generation 1中回收,依此类推,最高的generation是2,每次回收后存活下来的对象将升至更高级别的generation,从0到1再到2。寿命短的对象很自然的会在generation 0中回收,但如果延长它的寿命的话它就很可能会升至generation 1,这样一来就没有generation 0的回收效率了。

不好的做法:

static void Foo() {
   
string lines = "";
   
for (int i = 1; i <= 10; i++{
      
// 每次lines的临时内容都不能第一次就被
      
// 回收,因为lines有引用,所以这个临时对象
      
// 会被promote为更高的generation,这样无形
      
// 中就对GC的工作压力产生了巨大的影响。
      lines += "Line " + i + Environment.NewLine;
   }

   Console.Write(lines);
}

好的做法:

在这个特殊的例子中应使用StringBuilder避免临时对象的产生。

注意字符串连接的做法。

为什么:

不同的字符串连接需采用不同的方法,这样才能达到不损伤性能。有以下几种情况:

  1. 连接string literal:采用+符号连接,编译时编译器会将这些被连接的literals自动写成一个literal。
  2. 一次性连接已知数量的literal与/或变量:采用+符号连接,因为这个连接其实只是一个String.Concat,没有临时对象产生。
  3. 多次连接或者连接未知数量的literal或变量:采用StringBuilder来追加内容,避免临时对象的产生。

不好的做法:

void Foo() {
   
// Case 1. 连接literals使用StringBuilder
   
// 要比用+号性能低很多。
   StringBuilder builder = new StringBuilder();
   builder.Append(
"This is a very very ");
   builder.Append(
"very very very very ");
   builder.Append(
"very very very very ");
   builder.Append(
"very very very very ");
   builder.Append(
"long string literal");
   Console.Write(builder.ToString());
   
   
// Case 2. 一次性连接已知数量就足够的情况下
   
// 就不必使用StringBuilder了。
   StringBuilder builder2 = new StringBuilder();
   builder2.Append(
"Literal variable newline");
   builder2.Append(tempVar);
   builder2.Append(Environment.NewLine);
   Console.Write(builder2.ToString());
   
   
// Case 3.1. 这里每次+=都会产生两个临时字符串
   
// 对象,大大增加了GC的负担,严重影响了效率。
   string lines = "";
   
for (int i = 1; i <= 10; i++{
      lines 
+= "Line " + i + Environment.NewLine;
   }

   Console.Write(lines);
   
   
// Case 3.2. 这里每次Append时会由+引起临时字符串
   
// 对象,影响效率。
   StringBuilder builder = new StringBuilder();
   
for (int i = 1; i <= 10; i++{
      builder.Append(
"Line " + i + Environment.NewLine);
   }

   Console.Write(builder.ToString());
}

好的做法:

void Foo() {
   
// Case 1.
   string longLiteral =
      
"This is a very very " +
      
"very very very very " +
      
"very very very very " +
      
"very very very very " +
      
"long string literal";
   Console.Write(longLiteral);
   
   
// Case 2.
   string line =
      
"Literal variable newline" +
      tempVar 
+ Environment.NewLine;
   Console.Write(line);
   
   
// Case 3.
   StringBuilder builder = new StringBuilder();
   
for (int i = 1; i <= 10; i++{
      builder.Append(
"Line ");
      builder.Append(i);
      builder.Append(Environment.NewLine);
   }

   Console.Write(builder.ToString());
}

不采用ToUpper或ToLower进行字符串不区分大小写的对比。

为什么:

ToUpper与ToLower每次对比时会产生临时对象,影响性能。改用String.Compare,这不会有临时对象产生,而且还可以根据文化进行对比(或文化中立的进行对比)。

不好的做法:

static void Foo(string s1, string s2) {
   
// 这里会有两个临时字符串对象产生。
   if (s1.ToLower() == s2.ToLower()) {
      
   }

}

好的做法:

static void Foo(string s1, string s2) {
   
if (string.Compare(s1, s2, false)) {
      
   }

}

To be continued...

posted @ 2005-12-16 13:39 Cavingdeep 阅读(1519) 评论(21)  编辑 收藏 所属分类: Coding

  回复  引用  查看    
#1楼2005-12-16 14:31 | Laser.NET      
好文章,获益匪浅,尤其是关于 字符串的连接操作 上都没有注意过编译器会做笔者讲的优化。期待续篇:)
  回复  引用  查看    
#2楼2005-12-16 14:44 | birdshome      
过于教条了
不同的优化总是只能适合一定的场景
我们考虑效率也不是孤立的
效率有很多种,运行的效率、开发的效率、测试的效率,到底我们需要什么呢?
确定了我们所想要得,才能选出真正合适的"High Performance"代码。

  回复  引用  查看    
#4楼[楼主]2005-12-16 16:29 | Cavingdeep      
@birdshome

以上提出的决不是只适合一定的场景,它们适合任意场景,这是编码性能与其他性能的不同之处。编码时注意性能应该是一件很平常的事情,不应该需要额外的花费,所以它也不应当影响其他的效率。还有你指出的是开发作为流程上的效率,与代码效率是不一样的,本文给出的指南是基于代码的考虑。

@lovecherry

感谢提供这么好的链接,我想每个人都应该仔细读一下,微软已经做了很好的总结了(不过不是全部都适用,有的很牵强)。.NET已经提供了很好的性能优化,如果你的程序性能不高,千万别说是因为是.NET程序所以才慢,找原因从自己找起!

  回复  引用  查看    
#5楼2005-12-16 16:56 | Teddy's Knowledge Base      
精彩!
  回复  引用  查看    
#6楼2005-12-16 17:56 | 装配脑袋      
Dispose那里有一点不清楚,读者会闹不明白StreamWrite这算托管还是非托管。其实在if(disposing)的外边应该是释放“纯手工非托管”资源,典型就是IntPtr形式存在的资源。你举个例子就好了。

现在VS2005只要一实现IDisposable就自动把Disposing-Finalize模式实现,至少VB是这样。

  回复  引用  查看    
#7楼2005-12-16 18:05 | 装配脑袋      
而且对象一旦Dispose就废掉了,如果再使用该对象应该报错(使用了纯手工非托管资源的要绝对绝对注意这一点!),Dispose-Finalizing无法自动帮你杜绝这种状况。所以最好加一个已经Dispose的标记
Class A
  Implements IDisposable

  Private disposedValue As Boolean = False ' To detect redundant calls

  ' IDisposable
  Protected Overridable Sub Dispose(ByVal disposing As Boolean)
    If Not Me.disposedValue Then
      If disposing Then
        ' TODO: 这里的代码应当是调用成员变量的Close, Dispose等
        ' 还可以执行其他自定义托管代码
      End If

      ' TODO: 用纯手工的方法,如Marshal那些函数,或者PInvoke的函数
      ' 直接释放掉IntPtr形式存在的资源
      ' 禁止在这里调用任何成员(包括父类的Dispose)的托管方法!

      If disposing Then
        ' 如果要调用父类的Dispose,在此调用
        ' MyBase.Dispose(true)
      End If

    End If
    Me.disposedValue = True
  End Sub

#Region " IDisposable Support "
  ' This code added by Visual Basic to correctly implement the disposable pattern.
  Public Sub Dispose() Implements IDisposable.Dispose
    ' Do not change this code. Put cleanup code in Dispose(ByVal disposing As Boolean) above.
    Dispose(True)
    GC.SuppressFinalize(Me)
  End Sub
  Protected Overrides Sub Finalize()
    Dispose(False)
    MyBase.Finalize()
  End Sub
#End Region

End Class

  回复  引用  查看    
#8楼2005-12-16 18:22 | 装配脑袋      
如果没有使用纯手工的IntPtr形态的非托管资源,正如楼主所说,应该最好不要声明Finalize。这时候应该可以使用一种“不带Finalize的Dispose模式”吧?楼主以为如何?
  回复  引用  查看    
#9楼2005-12-16 18:37 | birdshome      
以上提出的决不是只适合一定的场景,它们适合任意场景,这是编码性能与其他性能的不同之处。
--------------------------------
主观臆测了吧,就你上面的说到的StringBuilder其实并没有你想得那么好,而ToUpper()和ToLower()也完全没有你想得那么差:)

  回复  引用  查看    
#10楼2005-12-16 18:44 | 装配脑袋      
@birdshome

按你上次的实验结果,带有忽略大小写的String.Compare和使用ToUpper之类应该是一样差的性能。不过ToUpper会生成临时string,加重GC负担

哦,对了,你以前还做过实验,StringBuilder在建立的过程中消耗极大,几乎抵消掉StringBuilder.Replace函数所带来的高性能

  回复  引用  查看    
#11楼2005-12-16 19:01 | birdshome      
@装配脑袋
String.Compare那个没有ToUpper那个快,差别是一个数量级@_@。
其实在托管环境中,除了value type的box外,string就是最高效简单的类了,GC string带来负担是在string碎片太多了的情况下才明显。
我们知道string的本质就是"不可变"得内存块,我觉得GC它和"回收"一个value type的代价差不了太多。所以string GC的负担应该来自CLR对内存的整理,这种整理在string碎片量大的时候应该很恐怖。

  回复  引用  查看    
#12楼2005-12-16 19:03 | 装配脑袋      
@birdshome
是这样吗……我忘了具体数值了。你上次数据都是什么什么Ex,什么什么Ex2的,记不住……

不过FxCop建议我用String.Compare耶,还是Performance中的一条

  回复  引用  查看    
#13楼[楼主]2005-12-17 11:36 | Cavingdeep      
@装配脑袋
你的Dispose稍微有点问题。调用基类的Dispose不应该只在disposing为真时才调用,因为基类的非托管资源也需要清理,在任何时候。

关于被dispose的标记是对的,不过不是任何时候都必须这样做的,在一些简单的情况下就不需要。另外dispose标记不一定要用bool来做,可能考虑state模式也是一个不错的选择,尤其是当你的类型本来就拥有很多state(状态)的时候。^_^

  回复  引用  查看    
#14楼2005-12-17 16:06 | 装配脑袋      
你忘了一点,父类的Finalize是一定会紧接自己的Finalize调用的(VB可以手工控制这个顺序,C#不行),而父类的Finalize会调用自己的Dispose(false)。如果你那样写,父类的Dispose(false)就会被调用两次。我有bool作标记因此不会有这个问题,但是你没有,因此你那样写就可能有危险了。既然你已经解释了,应该在if disposing块里面调用成员(包括父类)的Dispose方法,就意味着不应该在if disposing块外面做这件事,不应该双重标准。
不要在Dispose(false)执行的范围里调用任何托管方法这是一条能够避免非常多难以想到问题的法则,你不会预料到Finalize在什么状态下发生,有可能Constructor都没执行完,有可能根本没执行到自己的Constructor。所以,如果你的程序是那样写的,我建议你重新审视一编。

  回复  引用  查看    
#15楼2005-12-17 16:14 | 装配脑袋      
不是Dispose(true)就清理托管资源,Dispose(false)就清理非托管资源。是Disposing(true)的时候两者都清理。因此我在if disposing的时候调用父类的Dispose不会有你假设的问题。

我编写过大量包装真正IntPtr的非托管资源的程序,所以我比较清楚做错之后的严重性。

  回复  引用  查看    
#16楼[楼主]2005-12-17 17:40 | Cavingdeep      
@装配脑袋
You are right! ;)

  回复  引用  查看    
#17楼2005-12-18 10:58 | 装配脑袋      
我又想了一下,如果父类已经实现了Dispose-Finalize模式,那么子类就没必要重新实现一遍。父类的Dispose(bool)方法做成virtual的,子类不再实现IDisposable.Dispose和Finalize,而是直接override父类的Dispose(bool)。这样会调用Dispose的Finalize就只有一份了,就可以直接在最后调用父类的Dispose而无需检查了。
  回复  引用    
#18楼2005-12-22 10:57 | A.Z[未注册用户]
内存回收是浪费性能的,不是占用大块内存的对象不要主动让GC回收,现在电脑内存都很大
  回复  引用    
#19楼2005-12-22 11:00 | A.Z[未注册用户]
非托管资源的句柄和非托管的内存要尽早释放。大多数都是独占的系统资源。
  回复  引用  查看    
#20楼2005-12-23 17:37 | 编写人生      
TO cavingdeep:
谢谢你的回答,我认为你的方法是将工厂注射进来,就是IOC,是个办法,但使用这里类的程序比较麻烦,相当与你调用windows的写文件方法,他需要你传入磁盘驱动程序对象。
我认为容器也是一种注入,不过是初始化的注入,使用者并不关心这个注入。
原文在:
http://tansm.cnblogs.com/archive/2005/12/23/303113.html">http://tansm.cnblogs.com/archive/2005/12/23/303113.html

  回复  引用  查看    
#21楼2005-12-23 17:50 | 编写人生      
装配脑袋:你也太能回复了吧。



发表评论

昵称: [登录] [注册]

主页:

邮箱:(仅博主可见)

评论内容:

  登录  注册

[使用Ctrl+Enter键快速提交评论]

0 298448




相关文章:

相关链接: