优化C#/。NET程序的提示

现在看来,优化是一种失落的艺术。 是不是所有程序员都从代码中挤出每一个效率的时间? 在雪地里行走五英里的时候经常这样做?

本着带回失落的艺术的精神,你知道一些简单的(或者可能是复杂的)改变来优化C#/ .NET代码的技巧吗? 既然它是如此广泛的东西,取决于一个人想要完成什么,这将有助于提供您的技巧的上下文。 例如:

  • 将多个字符串连接在一起时,请使用StringBuilder 。 请参阅底部的链接,了解这方面的注意事项。
  • 使用string.Compare比较两个字符串,而不是像string1.ToLower() == string2.ToLower()

迄今为止的普遍共识似乎是衡量的关键。 这种想法忽略了一个观点:测量并不能告诉你什么是错误的,或者如果遇到瓶颈,该怎么办。 我遇到了字符串连接瓶颈一次,不知道该怎么做,所以这些技巧是有用的。

我甚至发表这个观点的意思是要有一个共同瓶颈的地方,以及在碰到它们之前如何避免它们。 甚至不必关心任何人应该盲目追随的即插即用代码,更多的是要了解应该考虑性能,至少在某种程度上应该考虑到这一点,而且还有一些常见的陷阱需要注意。

我可以看到,虽然也知道为什么提示是有用的,应该在哪里应用。 对于StringBuilder提示,我发现了很久以前在Jon Skeet的网站上的帮助 。

现在看来,优化是一种失落的艺术。

曾经有一天,制造显微镜被当作艺术品进行制造。 光学原理知之甚少。 零件没有标准化。 管子,齿轮和镜头必须由高级技术人员手工制造。

这些天显微镜是作为一个工程学科生产的。 物理学的基本原理是非常好理解的,现成的零件是广泛可用的,显微镜建造工程师可以做出明智的选择,以如何最好地优化他们的仪器,以执行其设计的任务。

那个表现分析是一个“失落的艺术”,是一件非常非常好的事情。 那艺术是作为一种艺术来实践 。 应该寻求最佳的解决方案:通过认真应用固体工程原理解决工程问题

这些年来,我被问过几十次的“技巧和窍门”,人们可以用它来优化他们的vbscript /他们的jscript /他们的活动服务器页面/他们的VB /他们的C#代码。 我总是抵制这一点。 强调“技巧和诀窍”是正确的方法来处理性能。 这种方式导致代码难以理解,很难推理,很难维护,通常不会比相应的简单代码快得多。

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

  • 设定有意义的,可衡量的,以客户为中心的目标。
  • 构建测试套件,以在现实但可控和可重复的条件下针对这些目标测试您的性能。
  • 如果这些套件显示你没有达到你的目标,使用分析器等工具来找出原因。
  • 优化探查器所识别的性能最差的子系统。 对每一个变化进行分析,以便清楚地了解每个变化对性能的影响。
  • 重复以下三件事情之一:(1)达到目标并运送软件;(2)将目标向下修改为可实现的目标;(3)因无法实现目标而取消项目。

这与解决其他任何工程问题的方式是一样的,例如添加一个功能 – 为该功能设置以客户为中心的目标,跟踪制定稳定实施的进度,通过仔细的调试分析修复问题,持续迭代直到你运送或失败。 性能是一个功能。

对复杂的现代系统进行性能分析需要严格的纪律,注重坚实的工程原理,而不是一堆装满琐碎或不切实际情况的技巧。 我从来没有通过应用技巧和窍门来解决现实世界的性能问题。

获得一个好的分析器。

不要打扰,即使没有一个好的分析器来优化C#(真的,任何代码)。 它实际上有助于同时拥有一个采样和一个跟踪分析器。

如果没有一个好的分析器,你很可能会产生错误的优化,最重要的是,优化那些不是性能问题的例程。

分析的前三个步骤应始终是1)测量,2)测量,然后3)测量….

优化指南:

  1. 不要这样做,除非你需要
  2. 如果在问题上抛开新硬件而不是开发人员,那么不要这样做
  3. 除非您可以测量生产等效环境中的变化,否则不要这样做
  4. 除非您知道如何使用CPU 内存分析器,否则不要这样做
  5. 不要这样做,如果它会让你的代码不可读或不可维护

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

从最远端开始:使用YSlow来查看为什么你的网站对最终用户来说太慢了,然后回去修复你的数据库访问不是太宽(列)而不是太深(行)。

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

我也看到了一个“愚蠢”未优化算法打败了“聪明”优化算法的情况。 永远不要低估编译器和芯片设计人员如何将低效率的循环代码变成能够在流水线上完全在片上存储器中运行的超高效代码。 如果您认为“高效”,那么您的“巧妙的”基于树的算法的内部循环展开向后计数可能会被打败,因为它在执行期间无法保留在片内存储器中。 (请参阅关于测量的规则。)

在使用ORM时要注意N + 1选择。

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

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

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

好吧,我必须投入我最喜欢的工作:如果任务足够长以便进行人机交互,请在调试器中使用手动中断。

比。 一个分析器,这给你一个调用堆栈和变量值,你可以用来真正理解发生了什么事情。

做这10-20次,你会明白什么样的优化可能真的有所作为。

人们对于真正重要的事物有着有趣的想法 堆栈溢出是充满了关于,例如,是++ii++更“高性能”的问题。 下面是一个真正的性能调优的例子,对于任何语言来说,这个过程基本上都是一样的。 如果代码只是以某种方式写成的,“因为它更快”,这是猜测。

当然,你不是故意写愚蠢的代码,但如果猜测工作,将不需要分析器和分析技术。

如果你确定一个方法是一个瓶颈,但是你不知道该怎么做 ,那么你基本上就被卡住了。

所以我会列举一些事情。 所有这些东西都不是银色的子弹 ,你仍然需要分析你的代码。 我只是提出你可以做的事情的建议,有时可以帮助。 特别是前三个是重要的。

  • 尝试仅使用(或主要)低级类型或数组来解决问题。
  • 问题往往很小 – 使用一个聪明但复杂的算法并不总是让你赢,特别是如果不太智能的算法可以用只使用低级类型(数组)的代码来表示。 以n <= 100或者Tarjan的Dominator发现算法为例,使用InsertionSort vs MergeSort vs使用位向量来天真地解决n <= 100的问题的数据流形式。 (100当然只是给你一些想法 – 个人资料 !)
  • 考虑编写一个可以使用低级类型(通常是大小小于64的问题实例)解决的特殊情况,即使您必须保留其他代码以应对更大的问题实例。
  • 学习按位算术以帮助您完成上述两个想法。
  • BitArray可以是你的朋友,相比Dictionary,或者更糟的是,List。 但要小心,实施不是最佳的; 你可以自己写一个更快的版本。 而不是测试你的参数超出范围等,你可以经常构造你的算法,使索引不能超出范围 – 但你不能从标准的BitArray中删除支票, 它不是免费的
  • 作为一个例子,你可以用低级别类型的数组来做一个例子,BitMatrix是一个相当强大的结构,可以被实现为一个超大数组,甚至可以使用ulong作为“front”来遍历它,因为你可以把(与广度优先搜索中的队列相比较 – 但显然顺序是不同的,取决于项目的索引而不是纯粹的顺序)。
  • 除非右边是一个常数,除法和模数真的很慢。
  • 浮点数学一般不会慢于整数数学(不是“你可以做的事情”,而是“你可以跳过的东西”)
  • 分支不是免费的 。 如果你可以使用简单的算术(除了除法或模)来避免它,你有时可以获得一些性能。 将一个分支移到一个循环之外几乎总是一个好主意。

告诉编译器该做什么,而不是如何去做。 例如, 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];更好list.Sort(i => i.value); m = list[list.Count - 1]; list.Sort(i => i.value); m = list[list.Count - 1];

通过告诉系统你想要做什么,可以找出最好的方法来做到这一点。 LINQ是好的,因为它的结果是不计算的,直到你需要它们。 如果您只使用第一个结果,则不需要计算其余结果。

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

事实是,没有完美的优化代码这样的事情。 但是,您可以针对已知的CPU类型(和计数),已知平台(Microsoft? Mono ?),已知的框架/ BCL版本等已知系统(或系统组)上的特定部分代码进行优化。一个已知的CLI版本,一个已知的编译器版本(bug,规格改变,调整),已知数量的总计和可用内存,已知程序集起源( GAC ?disk?remote?),以及来自其他进程的已知后台系统活动。

在现实世界中,使用一个分析器,并查看重要的位; 通常情况下,显而易见的事情是任何涉及I / O的事情,涉及线程的任何事情(再次,这在版本之间变化很大)以及涉及循环和查找的任何事情,但是您可能会惊讶于“显然不好”的代码实际上不是问题,什么“明显好”的代码是一个巨大的罪魁祸首。

我没有真的尝试优化我的代码,但有时我会通过使用像反射器的东西把我的程序回到源代码。 然后比较我反射器将输出什么是有趣的。 有时我发现,我以更复杂的形式做了简化。 可能不会优化,但帮助我看到更简单的问题解决方案。