C#第一篇之性能优化

一、性能优化简介

              性能优化的目的是在保证程序运行结果正确的前提下,使程序运行的更快。程序性能主要体现为两方面,空间性能和时间性能,理论上分别用空间复杂度和时间复杂度来衡量。因为现在的存储设备原来越便宜了,所以,实践中大部分的优化工作都是想方设法用空间换时间。性能优化是一项工作量较大的工作,而且风险也较高,容易导致已有的程序逻辑出现漏洞。所以,在开始决定做性能优化前,需要确定待调优的程序代码是系统中的关键代码。


二、查找性能瓶颈的方式

  1. 获取内存消耗 
     
    // 获取创建一个字符串所需的内存空间
     
    long  start = GC.GetTotalMemory( true );
     
    stringName="Player1" ;
     
    GC.Collect();
     
    // 确保所有内存都被GC回收
     
    GC.WaitForFullGCComplete();
     
    long  end = GC.GetTotalMemory( true );
     
    long  useMemory = end - start;

      

  2. 获取时间消耗  
System.Diagnostics.Stopwatch watch = new  System.Diagnostics.Stopwatch();
watch.Start();
for  ( int  i = 0; i < 1000; i++)
{
      a++;
}
watch.Stop();
var  useTime = ( double )watch.ElapsedMilliseconds / 1000;

三丶性能优化实践


  • 优化程序结构 
    对于程序结构,在设计时就应该考虑,评估是否可以达到性能需求。如果后期发现了性能问题需要考虑调整结构会带来非常大的开销。举例:
    1. GcMultiRowGcMultiRow要支持100万行数据,假设每行有10列的话,就需要有1000万个单元格,每个单元格上又有很多的属性。如果不做任何优化的话,大数据量时,一个GcMultiRow软件的内存开销会相当的大。GcMultiRow采用的方案是使用哈希表来存储行数据。只有用户改过的行放到哈希表里,而对于大部分没有改过的行都直接使用模板代替。就达到了节省内存的目的。
    2. Spread for WPF/Silverlight (SSL)WPF的画法和Winform不同,是通过组合View元素的方法实现的。SSL同样支持百万级的数据量,但是又不能给每个单元格都分配一个View。所以SSL使用了VirtualizePanel来实现画法。思路是每一个View是一个Cell的展示模块。可以和Cell的数据模块分离。这样。只需要为显示出来的Cell创建View。当发生滚动时会有一部分Cell滚出屏幕,有一部分Cell滚入屏幕。这时,让滚出屏幕的Cell和View分离。然后再复用这部分View给新进入屏幕的Cell。如此循环。这样只需要几百个View就可以支持很多的Cell。
  • 缓存 
    缓存(Cache)是性能优化中最常用的优化手段.适用的情况是频繁的获取一些数据,而每次获取这些数据需要的时间比较长。这时,第一次获取的时候会用正常的方法,并且在获取之后把数据缓存下来。之后就使用缓存的数据。 如果使用了缓存的优化方法,需要特别注意缓存数据的同步,就是说,如果真实的数据发生了变化,应该及时的清除缓存数据,确保不会因为缓存而使用了错误的数据。 举例:
    1. 使用缓存的情况比较多。最简单的情况就是缓存到一个Field或临时变量里。 
       
         
      for int  i = 0; i < gcMultiRow.RowCount; i++)
      {
           // Do something;
      }
       
      以上代码一般情况下是没有问题的,但是,如果GcMultiRow的行数比较大。而RowCount属性的取值又比较慢的时候就需要使用缓存来做性能优化。           
       
      int  rowCount = gcMultiRow.RowCount;
      for  ( int  i = 0; i < rowCount; i++)
      {
          // Do something;
      }

        

    2. 使用对象池也是一个常见的缓存方案,比使用Field或临时变量稍微复杂一点。 例如,在MultiRow中,画边线,画背景,需要用到大量的Brush和Pen。这些GDI对象每次用之前要创建,用完后要销毁。创建和销毁的过程是比较慢的。GcMultiRow使用的方案是创建一个GDIPool。本质上是一些Dictionary,使用颜色做Key。所以只有第一次取的时候需要创建,以后就直接使用以前创建好的。以下是GDIPool的代码: 
      public  static  class  GDIPool
      {
           Dictionary<Color, Brush > _cacheBrush = new  Dictionary<Color, Brush>();
           Dictionary<Color, Pen> _cachePen = new  Dictionary<Color, Pen>();
           public  static  Pen GetPen(Color color)
          {
              Pen pen;
              if_cachePen.TryGetValue(color, out  pen))
              {
                  return  pen;
              }
              pen = new  Pen(color);
             _cachePen.Add(color, pen);
              return  pen;
          }
      }

        

    3. 懒构造 
      有时候,有的对象创建需要花费较长时间。而这个对象可能并不是所有的场景下都需要使用。这时,使用赖构造的方法可以有效提高性能。 举例:对象A需要内部创建对象B。对象B的构造时间比较长。 一般做法: 
      public  class  A
      {
          public  B _b = new  B();
      }

        

      一般做法下由于构造对象A的同时要构造对象B导致了A的构造速度也变慢了。优化做法:

      public  class  A
      {
          private  B _b;
          public  B BProperty
          {
              get
             {
                if (_b == null )
                {
                    _b = new  B();
                }
                return  _b;
             }
          }
      }

        

      优化后,构造A的时候就不需要创建B对象,只有需要使用的时候才需要构造B对象。
    4. 优化算法 
      优化算法可以有效的提高特定操作的性能,使用一种算法时应该了解算法的适用情况,最好情况和最坏情况。 以GcMultiRow为例,最初MultiRow的排序算法使用了经典的快速排序算法。这看起来是没有问题的,但是,对于表格软件,用户经常的操作是对有序表进行排序,如顺序和倒序之间切换。而经典的快速排序算法的最差情况就是基本有序的情况。所以经典快速排序算法不适合MultiRow。最后通过改的排序算法解决了这个问题。改进的快速排序算法使用了3个中点来代替经典快排的一个中点的算法。每次交换都是从3个中点中选择一个。这样,乱序和基本有序的情况都不是这个算法的最坏情况,从而优化了性能。
    5. 了解Framework提供的数据结构 
      我们现在工作的.net framework平台,有很多现成的数据数据结构。我们应该了解这些数据结构,提升我们程序的性能: 举例:
      1. string 的加运算符 VS StringBuilder: 字符串的操作是我们经常遇到的基本操作之一。 我们经常会写这样的代码 string str = str1 + str2。当操作的字符串很少的时候,这样的操作没有问题。但是如果大量操作的时候(例如文本文件的Save/Load, Asp.net的Render),这样做就会带来严重的性能问题。这时,我们就应该用StringBuilder来代替string的加操作。
      2. Dictionary VS List Dictionary和List是最常用的两种集合类。选择正确的集合类可以很大的提升程序的性能。为了做出正确的选择,我们应该对Dictionary和List的各种操作的性能比较了解。 下表中粗略的列出了两种数据结构的性能比较。 

        操作

        List

        Dictionary

        索引

        Find(Contains)

        Add

        Insert

        Remove

      3. TryGetValue 对于Dictionary的取值,比较直接的方法是如下代码: 
        if (_dic.ContainKey( "Key" )
        {
             return  _dic\[ "Key" \];
        }

          

        当需要大量取值的时候,这样的取法会带来性能问题。优化方法如下:

        object  value;
        if (_dic.TryGetValue( "Key" , out  value))
        {
             return  value;
        }

          

        使用TryGetValue可以比先Contain再取值提高一倍的性能。

      4. 为Dictionary选择合适的Key。 Dictionary的取值性能很大情况下取决于做Key的对象的Equals和GetHashCode两个方法的性能。如果可以的话使用Int做Key性能最好。如果是一个自定义的Class做Key的话,最好保证以下两点:1. 不同对象的GetHashCode重复率低。2. GetHashCode和Equals方法立即简单,效率高。
      5. List的Sort和BinarySearch性能很好,如果能满足功能需求的话推荐直接使用,而不是自己重写。 
        List< int > list = new  List< int >{3, 10, 15};
        list.BinarySearch(10); // 对于存在的值,结果是1
        list.BinarySearch(8); // 对于不存在的值,会使用负数表示位置,如查找8时,结果是-2, 查找0结果是-1,查找100结果是-4.

          

    6. 通过异步提升响应时间
      1. 多线程 
        有些操作确实需要花费比较长的时间,如果用户的操作在这段时间卡死会带来很差的用户体验。有时候,使用多线程技术可以解决这个问题 举例: CalculatorEngine在构造的时候要初始化所有的Function。由于Function比较多,初始化时间会比较长。这是就用到了多线程技术,在工作线程中做Function的初始化工作,就不影响主线程快速响应用户的其他操作了。代码如下: 
        public  CalcParser()
        {
            if  (_functions == null )
            {
                lock  (_obtainFunctionLocker)
                {
                    if  (_functions == null )
                    {
                        System.Threading.ThreadPool.QueueUserWorkItem((s) =>
                        {
                            if  (_functions == null )
                            {
                                lock  (_obtainFunctionLocker)
                                {
                                    if  (_functions == null )
                                    {
                                        _functions = EnsureFunctions();
                                    }
                                }
                            }
                        });
                    }
                }
            }
        }

          

        这里比较慢的操作就是EnsureFunctions函数,是在另一个线程里执行的,不会影响主线程的响应。当然,使用多线程是一个比较有难度的方案,需要充分考虑跨线程访问和死锁的问题。

      2. 加延迟时间 
        在GcMultiRow实现AutoFilter功能的时候使用了一个类似于延迟执行的方案来提升响应速度。AutoFilter的功能是用户在输入的过程中根据用户的输入更新筛选的结果。数据量大的时候一次筛选需要较长时间,会影响用户的连续输入。使用多线可能是个好的方案,但是使用多线程会增加程序的复杂度。MultiRow的解决方案是当接收到用户的键盘输入消息的时候,并不立即出发Filter,而是等待0.3秒。如果用户在连续输入,会在这0.3秒内再次收到键盘消息,就再等0.3秒。直到连续0.3秒内没有新的键盘消息时再触发Filter。保证了快速响应用户输入的目的。
      3. Application.Idle事件 
        在GcMultiRow的Designer里,经常要根据当前的状态刷新ToolBar上按钮的Disable/Enable状态。一次刷新需要较长的时间。如果用户连续输入会有卡顿的感觉,影响用户体验。GcMultiRow的优化方案是挂系统的Application.Idle事件。当系统空闲的时候,系统会触发这个事件。接到这个事件表示此时用户已经完成了连续的输入,这时就可以从容的刷新按钮的状态了。
      4. Invalidate, BeginInvoke. PostEvent 平台本身也提供了一些异步方案。 
        例如;在Winform下,触发一块区域重画的时候,一般不适用Refresh而是Invalidate,这样会触发异步的刷新。在触发之前可以多次Invalidate。BeginInvoke,PostMessage也都可以触发异步的行为。
    7. 了解平台特性 
      如WPF的DP DP相对于CLR property来说是很慢的,包括Get和Set都很慢,这和一般质感上Get比较快Set比较慢不一样。如果一个DP需要被多次读取的话建议是CLR property做Cache。
    8. 进度条,提升用户体验 
      有时候,以上提到的方案都没有办法快速响应用户操作,进度条,一直转圈圈的图片,提示性文字如"你的操作可能需要较长时间请耐心等待"。都可以提升用户体验。可以作为最后方案来考虑。



  • 优化程序结构 
    对于程序结构,在设计时就应该考虑,评估是否可以达到性能需求。如果后期发现了性能问题需要考虑调整结构会带来非常大的开销。举例:
    1. GcMultiRowGcMultiRow要支持100万行数据,假设每行有10列的话,就需要有1000万个单元格,每个单元格上又有很多的属性。如果不做任何优化的话,大数据量时,一个GcMultiRow软件的内存开销会相当的大。GcMultiRow采用的方案是使用哈希表来存储行数据。只有用户改过的行放到哈希表里,而对于大部分没有改过的行都直接使用模板代替。就达到了节省内存的目的。
    2. Spread for WPF/Silverlight (SSL)WPF的画法和Winform不同,是通过组合View元素的方法实现的。SSL同样支持百万级的数据量,但是又不能给每个单元格都分配一个View。所以SSL使用了VirtualizePanel来实现画法。思路是每一个View是一个Cell的展示模块。可以和Cell的数据模块分离。这样。只需要为显示出来的Cell创建View。当发生滚动时会有一部分Cell滚出屏幕,有一部分Cell滚入屏幕。这时,让滚出屏幕的Cell和View分离。然后再复用这部分View给新进入屏幕的Cell。如此循环。这样只需要几百个View就可以支持很多的Cell。
  • 缓存 
    缓存(Cache)是性能优化中最常用的优化手段.适用的情况是频繁的获取一些数据,而每次获取这些数据需要的时间比较长。这时,第一次获取的时候会用正常的方法,并且在获取之后把数据缓存下来。之后就使用缓存的数据。 如果使用了缓存的优化方法,需要特别注意缓存数据的同步,就是说,如果真实的数据发生了变化,应该及时的清除缓存数据,确保不会因为缓存而使用了错误的数据。 举例:
    1. 使用缓存的情况比较多。最简单的情况就是缓存到一个Field或临时变量里。 
       
         
      for int  i = 0; i < gcMultiRow.RowCount; i++)
      {
           // Do something;
      }
       
      以上代码一般情况下是没有问题的,但是,如果GcMultiRow的行数比较大。而RowCount属性的取值又比较慢的时候就需要使用缓存来做性能优化。           
       
      int  rowCount = gcMultiRow.RowCount;
      for  ( int  i = 0; i < rowCount; i++)
      {
          // Do something;
      }

        

    2. 使用对象池也是一个常见的缓存方案,比使用Field或临时变量稍微复杂一点。 例如,在MultiRow中,画边线,画背景,需要用到大量的Brush和Pen。这些GDI对象每次用之前要创建,用完后要销毁。创建和销毁的过程是比较慢的。GcMultiRow使用的方案是创建一个GDIPool。本质上是一些Dictionary,使用颜色做Key。所以只有第一次取的时候需要创建,以后就直接使用以前创建好的。以下是GDIPool的代码: 
      public  static  class  GDIPool
      {
           Dictionary<Color, Brush > _cacheBrush = new  Dictionary<Color, Brush>();
           Dictionary<Color, Pen> _cachePen = new  Dictionary<Color, Pen>();
           public  static  Pen GetPen(Color color)
          {
              Pen pen;
              if_cachePen.TryGetValue(color, out  pen))
              {
                  return  pen;
              }
              pen = new  Pen(color);
             _cachePen.Add(color, pen);
              return  pen;
          }
      }

        

    3. 懒构造 
      有时候,有的对象创建需要花费较长时间。而这个对象可能并不是所有的场景下都需要使用。这时,使用赖构造的方法可以有效提高性能。 举例:对象A需要内部创建对象B。对象B的构造时间比较长。 一般做法: 
      public  class  A
      {
          public  B _b = new  B();
      }

        

      一般做法下由于构造对象A的同时要构造对象B导致了A的构造速度也变慢了。优化做法:

      public  class  A
      {
          private  B _b;
          public  B BProperty
          {
              get
             {
                if (_b == null )
                {
                    _b = new  B();
                }
                return  _b;
             }
          }
      }

        

      优化后,构造A的时候就不需要创建B对象,只有需要使用的时候才需要构造B对象。
    4. 优化算法 
      优化算法可以有效的提高特定操作的性能,使用一种算法时应该了解算法的适用情况,最好情况和最坏情况。 以GcMultiRow为例,最初MultiRow的排序算法使用了经典的快速排序算法。这看起来是没有问题的,但是,对于表格软件,用户经常的操作是对有序表进行排序,如顺序和倒序之间切换。而经典的快速排序算法的最差情况就是基本有序的情况。所以经典快速排序算法不适合MultiRow。最后通过改的排序算法解决了这个问题。改进的快速排序算法使用了3个中点来代替经典快排的一个中点的算法。每次交换都是从3个中点中选择一个。这样,乱序和基本有序的情况都不是这个算法的最坏情况,从而优化了性能。
    5. 了解Framework提供的数据结构 
      我们现在工作的.net framework平台,有很多现成的数据数据结构。我们应该了解这些数据结构,提升我们程序的性能: 举例:
      1. string 的加运算符 VS StringBuilder: 字符串的操作是我们经常遇到的基本操作之一。 我们经常会写这样的代码 string str = str1 + str2。当操作的字符串很少的时候,这样的操作没有问题。但是如果大量操作的时候(例如文本文件的Save/Load, Asp.net的Render),这样做就会带来严重的性能问题。这时,我们就应该用StringBuilder来代替string的加操作。
      2. Dictionary VS List Dictionary和List是最常用的两种集合类。选择正确的集合类可以很大的提升程序的性能。为了做出正确的选择,我们应该对Dictionary和List的各种操作的性能比较了解。 下表中粗略的列出了两种数据结构的性能比较。 

        操作

        List

        Dictionary

        索引

        Find(Contains)

        Add

        Insert

        Remove

      3. TryGetValue 对于Dictionary的取值,比较直接的方法是如下代码: 
        if (_dic.ContainKey( "Key" )
        {
             return  _dic\[ "Key" \];
        }

          

        当需要大量取值的时候,这样的取法会带来性能问题。优化方法如下:

        object  value;
        if (_dic.TryGetValue( "Key" , out  value))
        {
             return  value;
        }

          

        使用TryGetValue可以比先Contain再取值提高一倍的性能。

      4. 为Dictionary选择合适的Key。 Dictionary的取值性能很大情况下取决于做Key的对象的Equals和GetHashCode两个方法的性能。如果可以的话使用Int做Key性能最好。如果是一个自定义的Class做Key的话,最好保证以下两点:1. 不同对象的GetHashCode重复率低。2. GetHashCode和Equals方法立即简单,效率高。
      5. List的Sort和BinarySearch性能很好,如果能满足功能需求的话推荐直接使用,而不是自己重写。 
        List< int > list = new  List< int >{3, 10, 15};
        list.BinarySearch(10); // 对于存在的值,结果是1
        list.BinarySearch(8); // 对于不存在的值,会使用负数表示位置,如查找8时,结果是-2, 查找0结果是-1,查找100结果是-4.

          

    6. 通过异步提升响应时间
      1. 多线程 
        有些操作确实需要花费比较长的时间,如果用户的操作在这段时间卡死会带来很差的用户体验。有时候,使用多线程技术可以解决这个问题 举例: CalculatorEngine在构造的时候要初始化所有的Function。由于Function比较多,初始化时间会比较长。这是就用到了多线程技术,在工作线程中做Function的初始化工作,就不影响主线程快速响应用户的其他操作了。代码如下: 
        public  CalcParser()
        {
            if  (_functions == null )
            {
                lock  (_obtainFunctionLocker)
                {
                    if  (_functions == null )
                    {
                        System.Threading.ThreadPool.QueueUserWorkItem((s) =>
                        {
                            if  (_functions == null )
                            {
                                lock  (_obtainFunctionLocker)
                                {
                                    if  (_functions == null )
                                    {
                                        _functions = EnsureFunctions();
                                    }
                                }
                            }
                        });
                    }
                }
            }
        }

          

        这里比较慢的操作就是EnsureFunctions函数,是在另一个线程里执行的,不会影响主线程的响应。当然,使用多线程是一个比较有难度的方案,需要充分考虑跨线程访问和死锁的问题。

      2. 加延迟时间 
        在GcMultiRow实现AutoFilter功能的时候使用了一个类似于延迟执行的方案来提升响应速度。AutoFilter的功能是用户在输入的过程中根据用户的输入更新筛选的结果。数据量大的时候一次筛选需要较长时间,会影响用户的连续输入。使用多线可能是个好的方案,但是使用多线程会增加程序的复杂度。MultiRow的解决方案是当接收到用户的键盘输入消息的时候,并不立即出发Filter,而是等待0.3秒。如果用户在连续输入,会在这0.3秒内再次收到键盘消息,就再等0.3秒。直到连续0.3秒内没有新的键盘消息时再触发Filter。保证了快速响应用户输入的目的。
      3. Application.Idle事件 
        在GcMultiRow的Designer里,经常要根据当前的状态刷新ToolBar上按钮的Disable/Enable状态。一次刷新需要较长的时间。如果用户连续输入会有卡顿的感觉,影响用户体验。GcMultiRow的优化方案是挂系统的Application.Idle事件。当系统空闲的时候,系统会触发这个事件。接到这个事件表示此时用户已经完成了连续的输入,这时就可以从容的刷新按钮的状态了。
      4. Invalidate, BeginInvoke. PostEvent 平台本身也提供了一些异步方案。 
        例如;在Winform下,触发一块区域重画的时候,一般不适用Refresh而是Invalidate,这样会触发异步的刷新。在触发之前可以多次Invalidate。BeginInvoke,PostMessage也都可以触发异步的行为。
    7. 了解平台特性 
      如WPF的DP DP相对于CLR property来说是很慢的,包括Get和Set都很慢,这和一般质感上Get比较快Set比较慢不一样。如果一个DP需要被多次读取的话建议是CLR property做Cache。
    8. 进度条,提升用户体验 
      有时候,以上提到的方案都没有办法快速响应用户操作,进度条,一直转圈圈的图片,提示性文字如"你的操作可能需要较长时间请耐心等待"。都可以提升用户体验。可以作为最后方案来考虑。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值