性能主要指两个方面:内存消耗和执行速度。性能优化简而言之,就是在不影响系统运行正确性的前提下,使之运行地更快,完成特定功能所需的时间更短。 缓存
本文以.NET平台下的控件产品MultiRow为例,描述C#性能优化的实践。 性能优化
性能优化原则 数据结构
· 理解需求 多线程
MultiRow的一个性能需求是:“百万行数据绑定下平滑滚动。”整个MultiRow项目的开发过程一直在考虑这个目标。 异步
· 理解瓶颈 函数
99%的性能消耗是因为1%的代码形成的。大部分性能优化都是针对这1%的瓶颈代码进行的。具体实施也就分为两步:“发现瓶颈”和“消除瓶颈”。 工具
· 切忌过分 性能
性能优化自己是有成本的。这个成本不仅仅体如今作性能优化所付出的工做量,还包括为性能优化而写出复杂的代码致使额外的维护成本,好比引入新的Bug,额外的内存开销等。性能优化经常须要在收益和成本之间作出权衡。 测试
如何发现性能瓶颈
性能优化的第一步是发现性能瓶颈,下面是一些定位性能瓶颈的实践。
· 如何获取内存消耗
如下代码能够获取某个操做的内存消耗。
long start = GC.GetTotalMemory(true); // 在这里写须要被测试内存消耗的代码,例如,建立一个GcMultiRow var gcMulitRow1 = new GcMultiRow(); GC.Collect(); // 确保全部内存都被GC回收 GC.WaitForFullGCComplete(); long end = GC.GetTotalMemory(true); long useMemory = end - start;
· 如何获取时间消耗
如下代码能够获取某个操做时间消耗。
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次,取时间消耗的平均值以排除不稳定数据。
· ANTS Performance Profiler
ANTS Performance Profiler是款功能强大的性能检测软件。熟练使用这个工具,咱们能够快速准确的定位到有性能问题的代码。这是一款收费软件,会在IL中加入一些钩子用来记录时间,因此在分析时,软件的执行速度会比实际运行慢一些,得到的数据也所以并非百分之百的准确,还要结合其余技巧来分析程序的性能。
· CodeReview
CodeReview是发现性能问题的最后手段。CodeReview应该对产品的性能瓶颈尽量多的关注,确保该部分逻辑执行的尽量的快。
性能优化的方法和技巧
定位了性能问题后,解决的办法有不少。下面是一些性能优化的技巧和实践。
· 优化程序结构
在设计时就应该考虑产品结构是否能够达到性能需求。若是后期发现了性能问题,调整结构会带来很是大的开销。
例如:
GcMultiRow要支持100万行数据。假设每行有10列的话,就须要有1000万个单元格,每一个单元格上又有不少的属性。若是不作任何优化,大数据量时,一个GcMultiRow软件的内存开销会至关的大。GcMultiRow采用的方案是使用哈希表来存储行数据:只有用户改过的行放到哈希表里,大部分没有改过的行都直接使用模板代替。这就达到了节省内存的目的。
WPF平台和Silverlight平台的画法和Winform平台不一样,是经过组合Visual元素的方法实现的。SpreadGrid for WPF产品一样支持百万级的数据量,可是又不能给每一个单元格都分配一个View。因此SpreadGrid使用了VirtualizingPanel来实现画法。思路是每个Visual是一个Cell的展现模块,能够和Cell的数据模块分离,这样就只须要为显示出来的Cell建立Visual。当发生滚动时会有一部分Cell滚出屏幕,有一部分Cell滚入屏幕。这时,让滚出屏幕的Cell和Visual分离,而后再复用这部分Visual给新进入屏幕的Cell。如此循环,就只须要几百个Visual就能够支持不少的Cell。
· 缓存
缓存(Cache)是性能优化中最经常使用的手段,针对须要频繁的获取一些数据,同时每次获取数据须要的时间比较长的场景。若是使用了缓存的优化方法,须要特别注意缓存数据的同步:若是真实的数据发生了变化,应该及时的清除缓存数据,确保不会由于缓存而使用了错误的数据。
使用缓存的状况比较多。最简单的状况就是缓存到一个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; }
使用对象池也是一个常见的缓存方案,比使用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; } }
· 懒构造
大多时候,对于建立须要花费较长时间的对象,每每并非全部的场景下都须要使用。这时,使用懒构造的方法能够有效提升程序启动性能。
举例来讲,对象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对象,有效的提升了A的构造性能。
· 优化算法
优化算法能够有效的提升特定操做的性能。使用一种算法时应该了解算法的适用状况、最好状况和最坏状况。 以GcMultiRow为例,最初MultiRow的排序算法使用了经典的快速排序算法。这看起来是没有问题的。可是,对于表格软件,用户常常的操做是对有序表进行排序,如顺序和倒序之间切换。而经典的快速排序算法的最差状况就是基本有序的状况。因此经典快速排序算法不适合MultiRow。
改进的快速排序算法使用了3个中点来代替经典快排的一个中点的算法,每次交换都是从3个中点中选择中间值。这样,乱序和基本有序的状况都不是这个算法的最坏状况,从而优化了性能。
· 正确的使用既有数据结构
咱们如今工做的.NET framework平台有不少现成的数据结构。咱们应该了解这些数据结构,提高咱们程序的性能。
例如:
1. String的加运算符和StringBuilder: 字符串的操做是咱们常常遇到的基本操做之一。 咱们常常会写这样的代码 string str = str1 + str2。当操做的字符串不多的时候,这样的操做没有问题。可是若是大量操做的时候(例如文本文件的Save/Load, Asp.net的Render),这样作就会带来严重的性能问题。这时,咱们就应该用StringBuilder来代替string的加操做。
2. Dictionary和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; }
后一种用法要比前一种用法取值性能提升一倍。
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.
· 经过异步提高响应时间
1. 多线程
有些操做确实须要花费比较长的时间。在处理的过程当中,若是用户进行操做时失去响应,这个用户体验是不好的。使用多线程技术能够解决这个问题。例如,有一个相似Excel的计算引擎,在构造的时候要初始化全部的函数定义。因为函数比较多,初始化时间会比较长。这是若是用到了多线程,在工做线程中作函数定义进行的初始化,就不会影响到UI线程快速响应用户的其余操做了。
代码以下:
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. Refresh, BeginInvoke
平台自己也提供了一些异步方案。例如在WinForm下触发一块区域重画的时候,调用Refresh方法不会致使当即重画,而是设置Invalidate标记,触发异步的刷新。在控件开发中,这个技巧能够有效的提升产品的性能,同时简化实现复杂度。
Control.BeginInvoke方法能够被用来触发异步的自定义行为。
· 进度条,提高用户体验
有时候,以上提到的方案都没有办法快速响应用户操做。进度条、一直转圈圈的图片、提示性文字(如"你的操做可能须要较长时间,请耐心等待")等,均可以有效的提高用户体验,能够做为最后方案来考虑。