优化C#/。NET程序的技巧[关闭]


78

如今,优化似乎是一门失传的艺术。难道没有时间所有程序员都从他们的代码中压缩每一盎司的效率吗?在雪地里走五英里时经常这样做吗?

本着使人流连忘返的精神,您了解一些简单(或复杂)更改以优化C#/。NET代码的技巧吗?由于涉及的范围如此广泛,这取决于要完成的任务,因此有助于提供提示的上下文。例如:

  • 当将许多字符串连接在一起时,请StringBuilder改用。请参阅底部的链接以获取有关此方面的警告。
  • 使用string.Compare两个字符串比较,而不是做这样的事情string1.ToLower() == string2.ToLower()

到目前为止,似乎普遍存在的共识是关键。这种错误的意思是:测量并不能告诉您问题出在哪里,如果遇到瓶颈,该怎么办。我曾经遇到过字符串连接瓶颈,却不知道该怎么做,因此这些技巧很有用。

我发布此文章的目的是为常见的瓶颈留出一席之地,以及在遇到瓶颈之前如何避免它们。甚至没有人必须盲目遵循的即插即用代码,而是更多地了解应该至少在某种程度上考虑性能,并且要注意一些常见的陷阱。

我可以看到,虽然也可能知道技巧为什么有用以及应该在哪里应用,这可能会很有用。作为StringBuilder提示,我在Jon Skeet的站点上找到了我很早以前所做的帮助。


10
在优化和可读性之间走线也很重要。
瑞安·埃尔金斯

3
“少数字符串”;该数量不是问题-这是他们是否是在一个单一的复合置语句或多个语句。
Marc Gravell

2
StringBuilder通常比+运算符慢。C#编译器自动将重复+转换为String.Concat的适当重载。
理查德·伯格

2
在CLR运行时优化IL的过程中,您将不得不艰难应对CLR,并且您尝试在编译时进行同样的操作-拔河。在过去的好日子里,您对机器的说明进行了优化,并且机器笨拙地运行了它们。
约翰·K

Answers:


106

如今,优化似乎是一门失传的艺术。

曾经有一天,制造显微镜被视为一种艺术。光学原理知之甚少。没有零件的标准化。镜筒,齿轮和镜片必须由熟练的工人手工制作。

如今,显微镜是作为工程学科生产的。物理学的基本原理非常好理解,现成的零件种类繁多,显微镜制造工程师可以就如何最佳地优化仪器以完成其设计任务做出明智的选择。

绩效分析是一件“失落的艺术”,是一件非常非常好的事情。那门艺术被实践为一门艺术。应该对其进行优化:通过认真应用扎实的工程原理可以解决工程问题

多年来,我曾被问过数十次有关“提示和技巧”列表的信息,人们可以用来优化其vbscript / jscript /活动服务器页面/ VB / C#代码。我一直拒绝。强调“技巧”完全是错误的表现方式。 这种方式导致难以理解,难以推理,难以维护的代码通常并不比相应的直接代码明显快。

处理性能的正确方法是像处理其他任何问题一样将其作为工程问题处理:

  • 设定有意义,可衡量,以客户为中心的目标。
  • 构建测试套件,以在现实但可控且可重复的条件下针对这些目标测试您的性能。
  • 如果这些套件表明您没有达到目标,请使用探查器之类的工具找出原因。
  • 优化探查器识别为性能最差的子系统的功能。对每个更改进行概要分析,以便您清楚地了解每个更改对性能的影响。
  • 重复执行以下操作,直到发生以下三种情况之一:(1)您达到目标并交付了软件,(2)您将目标向下修订为可以实现的目标,或者(3)由于无法达到目标而取消了项目。

这与您解决任何其他工程问题一样,例如添加功能-为功能设置以客户为中心的目标,跟踪实现可靠实现的进度,通过仔细的调试分析找到问题后加以修复,不断进行迭代直到您发货或失败。性能是一项功能。

在复杂的现代系统上进行性能分析需要纪律,并专注于扎实的工程原理,而不是一full而就的技巧。我从来没有通过应用技巧来解决现实世界中的性能问题。


1
打算写一个类似的熨平板,但是你更好。太棒了
理查德·伯格

7
在某些情况下,存在一种已知的更好的方法来完成相同的任务,而又不会浪费很多资源。我不认为编写程序是完全可以的,但是只要您达到某个目标就可以了,而且看起来还可以。或者最好是进行编程,然后运行分析器,然后返回并更改问题区域。一个对在某些特定代码位甚至不启动之前就需要进行优化的人有什么好主意呢?
鲍勃2010年

5
@Bob:明智地使用资源没有错。出问题的地方是人们(1)在微优化上花费大量时间(=金钱)而没有改变;(2)编写错误的程序;(3)编写不清楚的程序。首先,您应该优化的是正确性。第二,良好的编码风格。第三,性能。一旦代码正确且优美,将使其更容易执行。
埃里克·利珀特

3
很好,但是您会发现我并不是说一个人不应该首先为正确性编码,或者不为样式编码,或者您拥有什么。但是,确实(有时,或者这几天可能很多次)程序员根本不考虑性能或优化。仅仅拥有1和2足以弥补3的总不足额吗?我看不出对优化表示敬意并学到一两个东西是个坏主意
Bob的

7
@Bob:我同意某些程序员不关心性能。但是我没有听你的意思。提示和技巧列表不会突然使它们变成关心性能的人。为了争辩,假设您可以使当前不感兴趣的人变成感兴趣的人,那么技巧和窍门列表并不能帮助他们获得良好的表现。您可以整天将提示和技巧应用于一段代码,而永远不知道自己是否在朝着目标前进。您必须有目标并衡量您的进度。
埃里克·利珀特

45

获取一个好的分析器。

即使没有良好的分析器,也不必尝试优化C#(实际上是任何代码)。实际上,手头有一个采样器和一个跟踪探查器实际上会大大帮助。

没有良好的分析器,您可能会创建错误的优化,最重要的是,优化一开始并不是性能问题的例程。

进行概要分析的前三个步骤应始终是1)测量,2)测量,然后3)测量...。


1
我会说,不要测量捕捉stackoverflow.com/questions/406760/...
麦克Dunlavey

23
您忘记了4) measure
Nifle

1
@Nifle:如果您在猎杀大象,您需要测量它们吗?
Mike Dunlavey 2010年

1
@RobbieDee:请参阅Conrad Albrecht的答案
Mike Dunlavey 2015年

1
@MikeDunlavey抱歉,我只是和你一起玩得很开心,但谢谢... :-)
Robbie Dee 2015年

21

优化准则:

  1. 除非需要,否则不要这样做
  2. 如果将新硬件而不是开发人员花在解决问题上更便宜,请不要这样做
  3. 除非您可以测量等同于生产环境的变化,否则不要这样做
  4. 除非您知道如何使用CPU内存探查器,否则不要这样做
  5. 如果这样做会使您的代码难以阅读或无法维护,请不要这样做

随着处理器的不断提高,大多数应用程序中的主要瓶颈不是CPU,而是带宽:片外内存的带宽,磁盘的带宽和网络的带宽。

从远端开始:使用YSlow来查看为什么网站对最终用户来说很慢,然后移回并修复您的数据库访问权限,使其访问范围(列)和宽度(行)都不太宽。

在极少数情况下值得采取任何措施来优化CPU使用率时,请注意不要对内存使用率产生负面影响:我已经看到“优化”,开发人员尝试使用内存来缓存结果以节省CPU周期。最终结果是减少了用于缓存页面和数据库结果的可用内存,这使应用程序运行速度大大降低!(请参阅有关测量的规则。)

我还看到过“哑巴”未优化算法击败了“灵巧”优化算法的情况。永远不要低估出色的编译器编写器和芯片设计器如何将“低效”循环代码转换为可以通过流水线完全在片内存储器中运行的超高效代码。您的“聪明”的基于树的算法具有未折叠的内部循环,您认为这是“有效的”,可以倒计时,因为它在执行期间无法停留在片内存储器中,因此可以击败它。(请参阅有关测量的规则。)


10
同样,不要沉迷于big-O分析。对于常见的业务案例,O(nm)天真字符串搜索算法比预处理搜索字符串以查找模式的O(n + m)算法快数千倍。匹配第一个字符的朴素字符串搜索通常会编译成一条机器指令,这在大量使用开放式内存缓存的现代处理器上非常快。
埃里克·利珀特

16

使用ORM时,请注意N + 1个选择。

List<Order> _orders = _repository.GetOrders(DateTime.Now);
foreach(var order in _orders)
{
    Print(order.Customer.Name);
}

如果客户不急于加载,则可能导致数次往返数据库。


14
  • 不要使用幻数,请使用枚举
  • 不要硬编码值
  • 由于类型安全,应尽可能使用泛型,并避免装箱和拆箱
  • 在绝对需要的地方使用错误处理程序
  • 处置,处置,处置。CLR不知道如何关闭数据库连接,因此请在使用后关闭它们并处置非托管资源
  • 使用常识!

15
我同意它们是一件好事,但前两件事对性能没有影响-只是可维护性...
Reed Copsey 2010年

是的,但这仍然是优化的代码。
SoftwareGeek 2010年

4
另外,第三(拳击)很少是真正的痛点。它被夸大为一个问题;例外情况-通常不是问题。
马克·格雷夫

1
“但这仍然是优化的代码”-这是一个很大的要求;我唯一想到的一个重要问题是“处置”;并且更有可能作为异常(超出句柄等)而不是性能下降而浮出水面。
马克·格雷夫

实际上,如果优化是您的目标,则终结器模式非常糟糕。具有终结器的对象将自动提升为Gen-1(或更糟)。此外,如果该Todo列表上有任何昂贵的东西,强制终结器代码在GC线程上运行通常不是最佳选择。底线:这是一项旨在方便和正确的功能,而不是旨在提高速度的功能。详细信息:msdn.microsoft.com/zh-cn/magazine/bb985010.aspx
Richard Berg,2010年

9

好的,我必须抛出我的最爱:如果任务足够长,可以进行人机交互,请在调试器中使用手动中断。

VS. 探查器,它为您提供了调用堆栈和变量值,您可以使用它们真正了解正在发生的事情。

这样做10到20次,您就会很好地了解哪些优化可能真正起作用。


1
++阿们。自从分析器存在之前,我一直在这样做。并且您的程序DrawMusic看起来很棒!
Mike Dunlavey 2010年

6
这实际上是探查器所做的事情,只是探查器以大约一千种不同的方式(更快,更频繁,更准确等)比您做得更好。他们也提供调用堆栈。这是穷人(和害怕学习新事物的老人)的解决方案。
BlueRaja-Danny Pflughoeft

@ BlueRaja-DannyPflughoeft:他们欺骗了你。他们非常准确地告诉您,没有什么事情要做。此方法与事件探查器之间的区别在于,在此方法中,您可以看到无法从简单统计信息中了解的要加速的事物。相反,如果实际上可以看到原始样本,则在前十个样本中可以明显看出可能导致问题的信息时,它们将抽取1000个样本。我确定你已经看过这篇文章
Mike Dunlavey 2013年

@ BlueRaja-DannyPflughoeft:看结果。您使用探查器获得的最大加速比是多少?
Mike Dunlavey

2
@ BlueRaja-DannyPflughoeft:我敢肯定你不会,当你达到我的年龄时,你会遇到像你这样的人。但是,让我们将其放在一边。这是一些源代码,如果您可以将其加速3个数量级,而无需查看我的操作方式,则可以使用任何其他方法来吹牛:)
Mike Dunlavey

9

如果您将方法确定为瓶颈,但又不知道该怎么做,那么您实际上就陷入了困境。

因此,我将列出一些事情。所有这些都不是灵丹妙药,您仍然必须分析您的代码。我只是为您可以做的事提供建议,有时可能会有所帮助。尤其是前三个很重要。

  • 尝试使用仅(或主要是)低级类型或它们的数组来解决问题。
  • 问题通常很小-使用智能但复杂的算法并不总能使您获胜,特别是如果较不智能的算法可以用仅使用低级类型(数组)的代码表示的话。例如,对于n <= 100,使用InsertionSort与MergeSort;对于n <= 100,则使用Tarjan的Dominator查找算法与使用位向量天真地解决问题的数据流形式。(当然100只是为了给您一些想法-个人资料!)
  • 考虑编写一种特殊情况,仅使用低级类型(通常大小小于64的问题实例)即可解决,即使对于较大的问题实例也必须保留其他代码。
  • 学习按位算术,以帮助您实现上述两个想法。
  • 与Dictionary相比,BitArray可以是您的朋友,或更糟糕的是,List。但是要注意,这种实现不是最佳的。您可以自己编写一个更快的版本。您可以经常构造算法以使索引无论如何都不会超出范围,而不是测试您的参数超出范围等,但是您不能从标准BitArray中删除该检查并且它不是免费的
  • 作为仅处理低级类型数组的示例,BitMatrix是一种功能非常强大的结构,可以将其作为ulong数组实现,甚至可以使用ulong作为“前”来遍历它,因为您可以恒定时间中的最低顺序位(与“广度优先搜索”中的“队列”相比-但显然顺序是不同的,并且取决于项目的索引,而不是纯粹按照找到它们的顺序)。
  • 除非右边是一个常数,否则除法和取模确实很慢。
  • 浮点运算是一般比整数运算慢了(而不是“东西,你可以做”,而是“东西,你可以跳过做”)
  • 分支不是免费的。如果您可以使用简单的算术(除以除法或取模以外的任何方法)来避免这种情况,则有时可以获得一些性能。将分支移到循环外部几乎总是一个好主意。

那里的一些好东西极大地帮助了我-谢谢!
罗比·迪

8

人们对真正重要的事情有一些有趣的想法。Stack Overflow充满了一些问题,例如,关于++i“性能”比“性能”更高i++这是一个实际性能调整的示例,对于任何语言,基本上都是相同的过程。如果只是简单地以某种方式编写代码“因为它更快”,那就是猜测。

当然,您不是故意编写愚蠢的代码,但是如果猜测可行,则不需要探查器和分析技术。


6

事实是,没有完美的优化代码。你可以,但是,优化特定部分的代码上众所周知的CPU类型,已知的系统(或系统组)(和计数),已知的平台(微软??),已知的框架/ BCL版本,已知的CLI版本,已知的编译器版本(错误,规范更改,调整),已知的总可用内存量,已知的程序集起源(GAC “磁盘”远程“)以及来自其他进程的已知后台系统活动。

在现实世界中,使用探查器并查看重要部分;通常,显而易见的事情是涉及I / O的事物,涉及线程的事物(同样,版本之间会有很大变化)以及涉及循环和查找的事物,但是您可能会对“显然不好的”代码实际上不是问题感到惊讶,而“明显好”的代码是一个巨大的罪魁祸首。


5

告诉编译器是什么做的,而不是如何去做。例如,foreach (var item in list)优于for (int i = 0; i < list.Count; i++)m = list.Max(i => i.value);优于list.Sort(i => i.value); m = list[list.Count - 1];

通过告诉系统您要执行的操作,可以找出执行操作的最佳方法。LINQ很好,因为直到需要时才计算其结果。如果仅使用第一个结果,则无需计算其余结果。

最终(这适用于所有编程)最大程度地减少了循环,并使循环中的工作最小化。更重要的是最大程度地减少循环内的循环数。O(n)算法和O(n ^ 2)算法有什么区别?O(n ^ 2)算法在循环内部有一个循环。


具有讽刺意味的是,LINQ添加了额外的香肠,应该怀疑是否存在没有它的解决方案。
user3800527 '16

2

我并不是真正地尝试优化我的代码,但有时我会经历并使用诸如反射器之类的程序将程序放回源代码。然后将我的错误与反射器的输出进行比较很有趣。有时,我发现我以更复杂的形式所做的工作得到了简化。可能无法优化事情,但可以帮助我看到更简单的问题解决方案。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.