C#性能优化实践

性能主要指两个方面:内存消耗和执行速度。性能优化简而言之,就是在不影响系统运行正确性的前提下,使之运行地更快,完成特定功能所需的时间更短。

\

本文以.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 \u0026lt; 1000; i++)\{\\tgcMultiRow1.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 \u0026lt; gcMultiRow.RowCount; i++)\{ \// Do something; \} \
\

以上代码一般情况下是没有问题的,但是,如果GcMultiRow的行数比较大。而RowCount属性的取值又比较慢的时候,就需要使用缓存来做性能优化。

\
\int rowCount = gcMultiRow.RowCount;\for (int i = 0; i \u0026lt; rowCount; i++)\{\// Do something;\}\
\

使用对象池也是一个常见的缓存方案,比使用Field或临时变量稍微复杂一点。例如,在MultiRow中,画边线,画背景,需要用到大量的Brush和Pen。这些GDI对象每次用之前要创建,用完后要销毁。创建和销毁的过程是比较慢的。GcMultiRow使用的方案是创建一个GDIPool。本质上是一些Dictionary,使用颜色做Key。所以只有第一次取的时候需要创建,以后就直接使用以前创建好的。

\

以下是GDIPool的代码:

\
\public static class GDIPool \{ \\tDictionary\u0026lt;Color, Brush \u0026gt; _cacheBrush = new Dictionary\u0026lt;Color, Brush\u0026gt;(); \\tDictionary\u0026lt;Color, Pen\u0026gt; _cachePen = new Dictionary\u0026lt;Color, Pen\u0026gt;(); \\tpublic static Pen GetPen(Color color) \\t{ \\t\tPen pen; \\t\tif_cachePen.TryGetValue(color, out pen)) \\t\t{ \\t\t\treturn pen; \\t\t} \\t\tpen = new Pen(color); \\t\t_cachePen.Add(color, pen); \\t\treturn pen; \\t} \}\
\

· 懒构造

\

大多时候,对于创建需要花费较长时间的对象,往往并不是所有的场景下都需要使用。这时,使用懒构造的方法可以有效提高程序启动性能。

\

举例来说,对象A需要内部创建对象B。对象B的构造时间比较长。 一般做法:

\
\public class A\{\\tpublic B _b = new B();\}\
\

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

\

优化做法:

\
\public class A\{\\tprivate B _b;\\tpublic B BProperty\\t{\\t\tget\\t\t{\\t\t\tif(_b == null)\\t\t\t{\\t\t\t\t_b = new B();\\t\t\t}\\t\t\treturn _b;\\t\t}\\t}\}\
\

优化后,构造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\")\{\\treturn _dic[\"Key\"];\}      \
\

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

\
\object value;\if(_dic.TryGetValue(\"Key\
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值