关于C#性能优化的介绍有很多资料,如《Effective C#》、《More Effective C#》以及互联网上的技术文档都有介绍,对这方面内容感兴趣的程序员推荐系统地阅读。本文,我主要介绍工作实践中对C#性能优化的体会、认识和实践。

1. 性能优化简介

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

2. 性能瓶颈诊断

实践证明,影响系统性能的代码往往是代码路径中非常少的部分代码,所以性能优化前的第一件事情就是诊断性能瓶颈所在。程序员可以借助专门的性能调优工具来诊断。工具中有收费的,也有免费的。这种工具一般都功能强大,可出性能报告,并有一定的学习曲线。如,ANTS Performance Profiler, VSTS Performance Profiler等。我个人认为大部分情况下,无需使用这么重的工具,使用C# StopWatch类即可统计出程序代码的时间消耗。代码如下:

System.Diagnostics.Stopwatch watch = new System.Diagnostics.Stopwatch();
watch.Start(); //开始计时
ExecBizLogic(); //执行程序逻辑
watch.Stop(); //结束计时
var timespan = watch.ElapsedMilliseconds ; //获取程序逻辑执行时间

3. 性能优化实践

无论何种程序语言,性能优化的方法主要有:优化系统结构、使用缓存、延后执行、优化算法和异步/多线程编程等。下面分别来举例说明:

a. 优化系统结构

在系统架构设计时,即要考虑系统的性能要求。例如,在Web系统中,用户要求网页的响应速度要再三秒以内。用Asp.Net Mvc实现时会发现很难达到,尤其是当后台传输给前台的数据较多,又频繁切换页面时。时间主要花在两方面:向后台请求数据以及根据Aspx页面中定义的Html结构渲染页面。细心的读者会发现,数据的获取是不可避免的工作,渲染页面的工作很多时候是重复的。其实,网站页面的结构是可穷举的,如果网页模板存储在前台,就不用每次都重新渲染了。所以,在确定系统展现层的结构时不应该选在Asp.Net Mvc的架构(后端模板绑定),而应该选择一种前端Mvc框架(前端模板绑定)以提高页面响应速度。

b. 使用缓存

使用缓存的思想是提高系统性能的法宝,绝大多数情况都是采用缓存来换取时间。之前,我曾经使用Office编程生产Word文档,其中有如下一段逻辑:

for( var i = 0; i < wordDoc.Ranges.Count; i++)
{
    //exce biz logic
}

结果发现随着文档页数的增加,速度会变得很慢。后来,我发现wordDoc.Ranges.Count这句话非常耗时,原因是C#代码调用Word对象模型需要从托管环境和非托管环境通信,而且每次循环都要执行。使用临时变量存储wordDoc.Ranges.Count的值,就会大大的改善程序性能。

再举一个例子,财务系统中经常需要计算某科目的余额。最简单的计算方法就是将该科目相关的所有交易进行累加,即可得到余额。计算每个被投公司收入占所有被投资公司的收入的比例,代码如下:

foreach( var company in companies)
{
    //calculate company income
    var income = Sum(company income records)
    //calculate all company income 
    var allIncome = Sum(all company income records)
    var percentage = income / allincome
}

从上述代码不难看出,每次循环都会重复计算allIcome的值,应采用临时变量将allIncome缓存住。当数据量非常大时,性能的提升也是非常可观的。

c. 延后执行

延后执行的思想是:只在有必要的情况才执行相关操作。延后执行思路的应用非常广泛。如,在用新浪微博的时候,屏幕上只会显示出当前屏幕内能显示的新微薄,当用户不断往下滑动屏幕时,再实时取数据。如果,一开始就加载上百条新微薄,系统将会变得很慢,降低用户体验,而且用户也不一定会一直往下看。此外,在经典的ORM框架NHibernate中,采用了Lazy Load机制,熟悉的人知道,如果没有Lazy Load,通过NHibernate从数据库取数据将是非常恐怖的。编程时,如果用Resharper来辅助自己,你会发现,Resharper经常会建议你将一些for/foreach循环改成Linq写法,原因就是,Linq采取延迟计算机制。来看下面一段代码:

//method 1
var clientNames = new List<string>();
foreach(var client in clients)
{
    clientNames.Add(client.Name);
}
//method 2
var clientNames = clients.Select(x => x.Name);

if(condition is true)
{
    //print all client name
}

不难看出,代码的含义是:满足一定条件后,就打印全部客户名称。如果采用method 1来收集客户名称,当不满足条件时,该工作将是无用功。而采用method 2时将不会浪费性能,因为他会延迟执行,直到打印的时候才会触发收集客户名称的动作。

d. 优化算法

算法的性能通常用时间复杂度来衡量,算法优化的目的即是降低程序的时间复杂度(从O(n ^ 3) –> O(n ^ 2))。如,将贪心算法改造成动态规划算法等,这要求程序员在深刻理解业务需求的情况下,运用算法分析与设计的知识,设计出性能良好的算法。仍然以财务系统为例,用户经常会对系统中Grid的数据进行排序,如将Grid中记录按时间顺序或逆序排列,并反复切换。排序算法一般会选择快排,然后在刚才的场景中,正是快排最差的情况,即数据基本有序。可以选择多个中点代替经典快排中一个中点的情况,以提高排序性能。

e. 异步/多线程编程

首先,我要强调,只有在对异步机制非常了解的情况下才进行异步编程。否则,花费大量的精力进行性能调优,不仅没有提高性能,还导致程序死锁,甚至出现错误。一般来讲,采用异步编程的前提条件是:系统需要处理大量任务,并且任务之间互相独立,没有依赖关系。典型的场景如批量发送邮件功能。每封邮件之间没有关联,成功发出去即可,此时采用异步机制发送邮件,在邮件数量庞大时,效果非常显著。仍然以上文财务系统中,计算每个被投公司收入占所有被投资公司的收入的比例的例子。前面,采用缓存allIcome字段的方法提高了程序的性能,其实还有提升的空间。因为计算每个公司的收入是独立,所以采用多线程方式计算每个公司的占比,性能将会大大提高。还有一种情况,如异步批量发送邮件结束,需要通知用户是否发送完毕,代码如下:

private volatile int count = 0;

public void Main(string[] args)
{
    for(var i = 0; i < 10; i++)
    {
        using(var smtpClient = new SmtpClient)
        {
            smtpClient.SendCompleted += new SendCompletedEventHandler(SendCompletedCallback);
            smtpClient.SendAsync(mailMessage, out userState);
        }
    }
    while(count < 10)
    {
        Thread.Sleep(100);
    }
    //send completed
}

private void SendCompletedCallback(object sender, AsyncCompletedEventArgs e)
{
    count++;
}

上述代码中,关键字volatile表示字段可能被多个并发执行线程修改。声明为 volatile 的字段不受编译器优化(假定由单个线程访问)的限制。这样可以确保该字段在任何时间呈现的都是最新的值。

4. 总结

性能优化的思路是相通的,上述方法在其他程序语言中也适用,只是因语法不通,实现方式有些许差别可以。文中所举的例子,接来自于项目实践的抽象,简化的过程中可能会有些不合理的地方,清楚意思即可。

posted on 2013-06-01 18:26  Maxwell Zhou  阅读(3228)  评论(0编辑  收藏  举报