zoukankan      html  css  js  c++  java
  • C#性能优化实践 1

    学习下 C#性能优化实践 (一)


    1. 根据经验,99%的性能消耗是由于1%的代码造成的。所以,大部分性能优化都是针对这1%的瓶颈代码进行的。具体实施也就分为两步。首先,确定瓶颈,其次消除瓶颈。
    2. 切忌过度 
      首先必须要认识到,性能优化本身是有成本的。这个成本不单单体现在做性能优化所付出的工作量。还包括为性能优化而写出的复杂代码,额外的维护成本,会引入新的Bug,额外的内存开销等。 一个常见问题是,一些刚接触软件开发的同学会对一些不必要的点生搬硬套性能优化技巧或者设计模式,带来不必要的复杂度。性能优化常常需要对收益和成本之间做出权衡。

    如何发现性能瓶颈

    上一节提到,性能优化的第一步就是发现性能瓶颈,这一节主要介绍定位性能瓶颈的一些实践。

    1. 如何获取内存消耗 
      以下代码可以获取某个操作的内存消耗。 
      // 在这里写一些可能消耗内存的代码,例如,如果想了解创建一个GcMultiRow软件需要多少内存可以执行以下代码
        
      long start = GC.GetTotalMemory(true);
        
      var gcMulitRow1 = new GcMultiRow();
        
      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++)
      {
           gcMultiRow1.Sort();
      }
      watch.Stop();
      var useTime = (double)watch.ElapsedMilliseconds / 1000;

       

      这里把一个操作循环执行了1000次,最后再把消耗的时间除以1000来确定最终消耗的时间。可以是结果更准确稳定,排除意外数据。

    3. 通过CodeReview发现性能问题。 
      很多情况下,可以通过CodeReview发现性能问题。对于大数据量的循环,要格外关注。循环内的逻辑应该执行的尽可能的快。
    4. ANTS Performance Profiler 
      ANTS Profiler是款功能强大的性能检测软件。可以很好的帮助我们发现性能瓶颈。使用这款软件定位性能瓶颈可以起到事半功倍的效果。熟练使用这个工具,我们可以快速准确的定位到有性能问题的代码。 这个工具很强大,但是也并不是完美无缺的。首先,这是一款收费软件,部门只有几个许可号。其次,这个软件的工作原理是在IL中加入一些钩子,用来记录时间。所以在分析时,软件的执行速度会比实际运行慢一些获得的数据也因此并不是百分之百的准确,应该把软件分析的数据作为参考,帮助快速定位问题,但是不要完全依赖,还要结合其他技巧来分析程序的性能。

     

    性能优化的方法和技巧

    定位了性能问题后,解决的办法有很多。这个章节会介绍一些性能优化的技巧和实践。

    1. 优化程序结构 
      对于程序结构,在设计时就应该考虑,评估是否可以达到性能需求。如果后期发现了性能问题需要考虑调整结构会带来非常大的开销。举例:
      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。
    2. 缓存 
      缓存(Cache)是性能优化中最常用的优化手段.适用的情况是频繁的获取一些数据,而每次获取这些数据需要的时间比较长。这时,第一次获取的时候会用正常的方法,并且在获取之后把数据缓存下来。之后就使用缓存的数据。 如果使用了缓存的优化方法,需要特别注意缓存数据的同步,就是说,如果真实的数据发生了变化,应该及时的清除缓存数据,确保不会因为缓存而使用了错误的数据。 举例:
      1. 使用缓存的情况比较多。最简单的情况就是缓存到一个Field或临时变量里。 
        
        
        forint 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. 进度条,提升用户体验 
        有时候,以上提到的方案都没有办法快速响应用户操作,进度条,一直转圈圈的图片,提示性文字如"你的操作可能需要较长时间请耐心等待"。都可以提升用户体验。可以作为最后方案来考虑
     
     
    分类: .NET

    C#跨线程修改控件——从MSIL和汇编看Invoke, 多线程, 事件与事件委托

    相信大家刚开始写winform的时候都遇到过这样的问题,当跨线程修改控件属性时会遇到如下的异常:

    线程间操作无效: 从不是创建控件"progressBar1"的线程访问它。

    这是相应的产生上述异常的代码:

     Director.cs
     Form1.cs

     

    我们知道当多个线程同时竞争资源的访问权并尝试修改资源状态时,资源可能出现同步异常。因此CLR才会禁止这种跨线程修改主窗体控件的行为。

    一个简单粗暴(但十分有效)的方法是在主窗体构造函数中加入CheckForIllegalCrossThreadCalls = false;

    像这样:

    public Form1()
    {            
        InitializeComponent();
        CheckForIllegalCrossThreadCalls = false;
    }

    附上msdn的解释:

    获取或设置一个值,该值指示是否捕获对错误线程的调用,这些调用在调试应用程序时访问控件的 Handle 属性。

    因此设为false后将不再检查非法跨线程调用。问题解决,本文也可以到此结束了(大误啊、、、)

    毕竟跨线程调用是不安全的,可能导致同步失败。所以我们采用正统一点的方法来解决,那就是调用control的Invoke()或BeginInvoke()方法。

    二者的差别在于BeginInvoke()是异步的,这里为了防止Director.Test()执行时主窗体关闭导致句柄失效进而产生异常,我们使用BeginInvoke()方法进行异步调用。

     更改过的Form1.cs

    至此,问题已经彻底解决,本文也可以真正地结束了。。。

    但是!!!我们都知道一个不想当Geek的码农不是好程序猿~

    于是乎我们应再次发扬Geek精神,剥去.NET粉饰的外衣,窥其真理的内核。

    先从Invoke()入手,看到其源码:

    public object Invoke(Delegate method, params object[] args)
    {
        using (new Control.MultithreadSafeCallScope())
        return this.FindMarshalingControl().MarshaledInvoke(this, method, args, true);
    }

    而BeginInvoke()差别仅仅在于MarshaledInvoke()的参数synchronous:

    复制代码
    public IAsyncResult BeginInvoke(Delegate method, params object[] args)
    {
          using (new Control.MultithreadSafeCallScope())
            return (IAsyncResult) this.FindMarshalingControl().MarshaledInvoke(this, method, args, false);
    }
    复制代码

    实质都是调用了MarshaledInvoke方法。Marshaled这个词常写NativeMethods的同学一定很熟悉。中文翻译我还真不知道,这里给出维基百科的释义作为参考:

    In computer science, marshalling (sometimes spelled marshaling with a single l) is the process of transforming the memory representation of an object to a data format suitable for storage or transmission, and it is typically used when data must be moved between different parts of a computer program or from one program to another. Marshalling is similar to serialization and is used to communicate to remote objects with an object, in this case a serialized object. It simplifies complex communication, using custom/complex objects to communicate instead of primitives. The opposite, or reverse, of marshalling is called unmarshalling (or demarshalling, similar to deserialization).

    所以.NET的“暗箱操作”很有可能就在MarshaledInvoke里面。我们点进去看一下,当然主要关注NativeMethods

     MarshaledInvoke

    果然被我们找到了,这个System.Windows.Forms.UnsafeNativeMethods.PostMessage()就是WinAPI封装过后的NativeMethod了。当然它披上另一件衣服之后也是MFC里面的CWnd::PostMessage, 负责向窗体消息队列中放置一条消息,并且不等待消息被处理而直接返回(即异步,这也是与SendMessage的差别)。(Places a message in the window's message queue and then returns without waiting for the corresponding window to process the message.)

    这也就解释了上述情况发生的原因,调用Invoke()而不是直接更改控件值使得主窗体能够将消息加入自身的消息队列中,从而在合适的时间处理消息,这样跨线程更改控件值就转变为窗体线程自己更改控件值,也就是从创建控件的线程(窗体主线程)访问控件,避免了之前的错误:“从不是创建控件"progressBar1"的线程访问它。”

    不过还有一个问题,如果本来就是窗体线程对控件进行访问呢,毫无疑问直接设置值即可。在上面的代码中我使用InvokeRequired属性来判断控件更改者是否来自于其他线程,从而决定是调Invoke()还是直接表白(无误)。那么这个属性是否真的如我们所想,仅仅是判断调用者线程呢?看代码:

     InvokeRequired

    还真是这么直白,最后的return写的非常清楚。

    至此,我们已经理解了Invoke的具体实现。下面来看事件委托,为什么Director.Test()能够触发Form1.cs的director_OnReport()回调函数。

    我们在Form1.cs中的button1_Click()函数中添加了回调director.OnReport += director_OnReport;于是Director类OnReport事件执行了add{_report += value;}完成添加回调绑定过程。基于上面的现象我们知道progressBar1是在非窗体线程被更改的(见Invoke实现),既然是来自非窗体线程的更改,那么会不会是本来在窗体类中的director_OnReport(string postStatus)函数在回调绑定完成之后直接被替换到了Director.Test()中的_report(counter.ToString(CultureInfo.InvariantCulture));呢?

    既然我们从表象上有理由怀疑这一点,那么就应当实际验证一下。只可惜C#封装的事件委托使得我们从.NET的源码中也难以知晓其底层实现。正所谓“不识庐山真面目,只缘身在此山中。”

    为了理解其底层实现,我们必须先走出C#语言层面这座山。那就先看看Director类的MSIL吧(话说MSIL现已被微软正名为CIL,微软一匡天下之心昭然若揭。。。)

     Director - MSIL

    OnReport事件的内容被编译为两个函数。我们先只看add_OnReport这个函数,无非是与Property的Getter和Setter类似,对内绑定到_report()函数。那么再来看Form1中对OnReport事件的注册:

     button1_Click - MSIL

    其中IL_0009:  ldftn位置到IL_000f:  newobj位置声明并实例化了director_OnReport作为委托的target,而IL_0014:  callvirt位置调用了add_OnReport()进行实际意义上的绑定。

    然后从IL_001b:  ldftn位置开始实例化新线程并进行相关赋值操作,直到IL_003b:  callvirt位置调用Thead::Start()运行线程。

    这样我们已经基本理清了绑定的实现过程,但是代码在执行时是否如我上面所说是“函数在回调绑定完成之后直接被替换”这样呢?想要验证就必须再看MSIL的底层实现,那是什么呢?对了,就是汇编。(//=_=老是自问自答有意思么。。。)

    打开高端大气上档次的反汇编界面,在Director类中设定断点:

    断点0:行26: add { _report += value; }

    断点1:行35: _report(counter.ToString(CultureInfo.InvariantCulture));

    开始调试,点击button1,第一次中断在断点0处:

     Director.cs - Asm

    其中Combine代码(无所谓了):

     Combine

     来到了断点1:

     Director.cs - Asm

    直接跳转到了函数director_OnReport()

     Form1.cs - Asm

    这就充分说明在C#代码层面上执行的_report()函数和director_OnReport()回调函数本质上是同一个函数(段地址相同),也恰好解释了为什么Form1类中的private函数为什么可以在另一个类中触发。因为C#也好,CIL也好,都是表层的封装。而在CLR虚拟机中实实在在运行的,是CLR Assembly. 我们说CLR是虚拟机,这个“虚拟”仅仅指CLR中的指令并非与物理硬件相关联,但是CLR以及其中的指令都是真实存在的,与真实机上的x86 CPU指令本质上是相同的。C#美轮美奂的亭台楼榭都建立在Assembly的一砖一瓦之上。而在CLR Assembly层面,只有内核级的概念,有内存管理,有线程调度。。。但是没有类级属性,没有成员函数,没有作用域可访问性控制。我们在使用C#封装好的模块和功能模型时,如果能够同时理解其底层实现,相信会对软件开发工作大有裨益。 

    忽然发现写了这么多。。而且好像逻辑很混乱的样子。。权当给小白入门看的吧~ 也欢迎各路大神不吝赐教。  另PS:这是本人的处女博(无误),以后要养成写博客的好习惯~

     
     
     
    标签: C#Winform多线程事件委托MSIL汇编

    面试经历和总结

    对于最近一个月的面试,一直想写点什么,却又不知道该写些什么,现在终于有勇气提起笔了。

    最近天气阴晴不定,变幻莫测,给人一种很压抑的感觉,再加上之前网易邮箱现场招聘、腾讯2013实习生招聘都以失败告终,心里难免会有一点小失落和伤心。其实自己心里很明白,现在的水平和能力只能被别人鄙视了,被刷了是很正常的,但多多少少会有一点不甘心,毕竟都好好准备过了。

    不管怎样,过去的就让它过去的,不能总是活在回忆中,这样就看不到未来了。虽然总是提醒自己不要难过,但一想起这些日子所经历的事情,心中就好像有一道跨不过的坎。现在只想好好平静下来,认真想想自己到底是哪里出问题了,哪些地方需要改进的,哪些方面需要总结的。我相信只要坚持梦想不放弃,总会到达成功的彼岸。从哪里跌倒就从哪里爬起来吧,收拾心情重新出发。

    最后附上面试时应该准备的东西和要注意的事项的一些总结:

    网易邮箱现场招聘、腾讯2013实习生招聘总结:
    (1)基础知识要扎实,体系要完整,对各个特性以及之间的差异要明白(如js部分的dom操作、event机制);
    (1)项目介绍表述要详细全面、脉络清晰、过度自然,最好在面试前准备充足,在脑海里形成了一个脉络,做到未雨绸缪;
    (2)交流谈话不能让面试官感觉浮夸,自以为是,就算懂的知识也要谦逊;
    (3)交流时语速要平缓,不能过快或过慢,语言组织和表达能力需要提高;
    (4)切忌紧张过度而使自己脑子一片空白,语无伦次((╯﹏╰)我就是紧张派);
    (5)主动向面试官提出问题,让对方感觉你脑子里是有东西的;
    (6)对于面试官提出的技术问题,分析解答时要有想法,有解决问题的能力。

    ps:希望对正在找工作的童鞋有所帮助,也希望自己继续加油,不要灰心,干巴爹~

    做一个有梦想,爱学习的好学生!

    因为分享,所以简单;因为分享,所以快乐。
     
    标签: 面试
  • 相关阅读:
    设计模式课程 设计模式精讲 2-1 本章导航
    数字 日期 格式化方法
    jQuery事件委托之Safari失效的解决办法--摘抄
    css3鼠标点击穿透--摘抄
    字符串日期转换为周
    在echars上发布的半圆环形图
    一些框架源码中的代码
    webSQL 增删改查
    Android 根据版本号更新
    Android 永久保存简单数据
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/3104984.html
Copyright © 2011-2022 走看看