【问题标题】:Tips for optimizing C#/.NET programs [closed]优化 C#/.NET 程序的技巧 [关闭]
【发布时间】:2011-01-29 05:48:24
【问题描述】:

如今,优化似乎是一门失传的艺术。不是所有程序员都从他们的代码中榨取每一盎司的效率吗?经常在雪地里行走 5 英里时这样做?

本着找回失传艺术的精神,您知道哪些关于通过简单(或可能是复杂)更改来优化 C#/.NET 代码的技巧?由于这是一个如此广泛的事情,这取决于一个人试图完成的事情,它有助于为您的提示提供上下文。例如:

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

目前的普遍共识似乎是衡量是关键。这种错失了重点:测量并不能告诉你哪里出了问题,或者如果你遇到瓶颈该怎么办。我曾经遇到过字符串连接瓶颈,不知道该怎么办,所以这些技巧很有用。

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

我可以看到,了解提示为何有用以及应在何处应用它可能很有用。对于StringBuilder 提示,我在here on Jon Skeet's site 找到了我很久以前所做的帮助。

【问题讨论】:

  • 在优化和可读性之间走一条线也很重要。
  • “少数字符串”; number 不是问题 - 问题在于它们是在单个复合串联语句中,还是在多个语句中。
  • StringBuilder 通常比 + 运算符慢。 C# 编译器自动将重复的 + 转换为 String.Concat 的适当重载。
  • 当 CLR 运行时优化 IL 时,您将不得不与 CLR 抗争,而您试图在编译时做同样的事情 - 拔河。在过去的美好时光里,你优化了机器的指令,机器默默地运行它们。

标签: c# .net optimization


【解决方案1】:

现在看来,优化似乎是一门失传的艺术。

曾经有一天,显微镜的制造被当作一门艺术来实践。光学原理知之甚少。零件没有标准化。管子、齿轮和镜头必须由技术精湛的工人手工制作。

如今,显微镜是作为一门工程学科生产的。物理学的基本原理得到了很好的理解,现成的零件随处可见,显微镜制造工程师可以做出明智的选择,以最佳方式优化他们的仪器以完成其设计执行的任务。

性能分析是一门“失传的艺术”,这是一件非常非常好的事情。这种艺术被实践作为一种艺术。应该针对它的本质进行优化:一个工程问题,可以通过仔细应用可靠的工程原理来解决。

多年来,我被问了几十次,要我列出人们可以用来优化他们的 vbscript/jscript/他们的活动服务器页面/他们的 VB/他们的 C# 代码的“提示和技巧”列表。我总是抗拒这一点。 强调“技巧和窍门”是完全错误的处理性能的方式。这样会导致代码难以理解、难以推理、难以维护,通常不会明显快于对应的直接代码。

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

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

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

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

【讨论】:

  • 本来打算写一个类似的熨平板,但你的更好。太棒了。
  • 在某些情况下,有一种已知的更好的方法来完成相同的任务,同时减少资源的消耗。我不认为编程是完全没问题的,但是只要你达到某个目标并且它似乎可以正常工作。或者为了最好的编程,然后运行分析器,然后然后返回并更改问题区域。一个人在开始之前就知道如何优化某些代码有什么问题?
  • @Bob:聪明地使用资源并没有错。出问题的地方是人们 (1) 花费大量时间 (=金钱) 进行没有影响的微优化,(2) 编写错误的程序,以及 (3)编写不清楚的程序。您首先应该优化的是正确性。其次,良好的编码风格。第三,性能。一旦代码正确且优雅,就更容易使其具有性能。
  • 这很好,但是你会注意到我并不是说一个人不应该首先编码正确性,或者第二个样式,或者你有什么。但是,有时(或者这些天可能很多次)程序员根本不考虑性能或优化也是事实。仅仅拥有 1 和 2 就足以弥补完全不关心的 3 吗?我看不出对优化表示敬意并学习一两件事是个坏主意
  • @Bob:我同意有些程序员不关心性能。但我没有遵循你的观点。提示和技巧列表不会突然将他们变成关心性能的人。假设您可以将目前不感兴趣的人变成感兴趣的人,那么一系列提示和技巧并不能帮助他们获得良好的表现。您可以整天对代码体应用提示和技巧,但永远不知道您是否在实现目标方面取得了任何进展。你必须有目标并衡量你的进步。
【解决方案2】:

获得一个好的分析器。

如果没有好的分析器,即使尝试优化 C#(实际上,任何代码)也不要费心。实际上,手头同时拥有采样和跟踪分析器会大有帮助。

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

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

【讨论】:

【解决方案3】:

优化指南:

  1. 除非需要,否则不要这样做
  2. 如果用新硬件而不是开发人员来解决问题更便宜,请不要这样做
  3. 除非您可以衡量生产等效环境中的变化,否则不要这样做
  4. 除非您知道如何使用 CPU和内存分析器
  5. ,否则不要这样做
  6. 如果它会使您的代码不可读或不可维护,请不要这样做

随着处理器越来越快,大多数应用程序的主要瓶颈不是 CPU,而是带宽:到片外内存的带宽、到磁盘的带宽和到网络的带宽。

从远端开始:使用 YSlow 查看您的网站对最终用户来说速度慢的原因,然后返回并修复您的数据库访问权限,使其不要太宽(列)和太深(行)。

在极少数情况下,需要采取任何措施来优化 CPU 使用率,请注意不要对内存使用产生负面影响:我见过开发人员尝试使用内存来缓存结果以节省 CPU 周期的“优化” .最终效果是减少了缓存页面和数据库结果的可用内存,这使得应用程序运行速度慢得多! (见测量规则。)

我还看到过“愚蠢”未优化算法击败“聪明”优化算法的情况。永远不要低估编译器编写者和芯片设计者在将“低效”循环代码转化为可以完全在片上内存中通过流水线运行的超高效代码方面的出色表现。您认为“高效”的“聪明”的基于树的算法具有展开的内部循环向后计数,因为它在执行期间未能留在片上存储器中,因此可能会被击败。 (见测量规则。)

【讨论】:

  • 同样,不要沉迷于大 O 分析。对于常见的业务案例,O(nm) 朴素字符串搜索算法比预处理搜索字符串以查找模式的 O(n+m) 算法快数千倍。匹配第一个字符的简单字符串搜索通常会编译为单个机器指令,这在大量使用乐观内存缓存的现代处理器上非常快。
【解决方案4】:

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

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

如果客户没有立即加载,这可能会导致多次往返数据库。

【讨论】:

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

    【讨论】:

    • 尽管我同意它们是一件好事,但这里的前两件事对性能没有影响 - 只是可维护性......
    • true 但它仍然是优化的代码。
    • 此外,第三(拳击)很少是真正的夹点;它被夸大为一个问题;例外情况 - 通常不是问题。
    • “但它仍然是一个优化的代码”——这是一个很大的主张;我唯一认为重要的问题是“处置”;这更有可能表现为异常(超出句柄等),而不是性能下降。
    • 实际上,如果您的目标是优化,那么终结器模式非常糟糕。带有终结器的对象会自动升级为 Gen-1(或更糟)。此外,如果 Todo 列表上有任何昂贵的东西,那么强制终结器代码在 GC 线程上运行通常不是最佳选择。底线:这是一项旨在方便和正确的功能,而不是旨在提高原始速度的功能。详情:msdn.microsoft.com/en-us/magazine/bb985010.aspx
    【解决方案6】:

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

    对比。分析器,它为您提供调用堆栈和变量值,您可以使用它们来真正了解正在发生的事情。

    这样做 10 到 20 次,您就会很好地了解哪些优化可能真正产生影响。

    【讨论】:

    • ++ 阿门。自从分析器存在之前,我就一直在这样做。你的程序 DrawMusic 看起来很棒!
    • 这基本上是分析器所做的,除了他们在大约一千种不同的方式(更快、更频繁、更准确等)上比你做得更好。他们也提供调用堆栈。这是穷人(以及害怕学习新事物的老人)的解决方案。
    • @BlueRaja-DannyPflughoeft:他们欺骗了你。他们非常准确地告诉你,没有什么可做的。此方法与分析器之间的区别在于,在此方法中,您可以看到无法从简单统计中梳理出来的要加速的事物。相反,如果您实际上可以看到原始样本,那么当可以引导您解决问题的信息在前 10 个中很明显时,他们会抽取 1000 个样本。我相信你已经看过this post。
    • @BlueRaja-DannyPflughoeft:看看结果。您使用分析器获得的最大加速比是多少?
    • @BlueRaja-DannyPflughoeft:我敢肯定你不会,当你到了我这个年纪,你会遇到像你这样的人。但是,让我们把它放在一边。 Here's some source code如果你能把它提速3个数量级,不用看我是怎么做到的,用任何其他方法,你就有吹牛的权利了:)
    【解决方案7】:

    如果您将某个方法确定为瓶颈,但您不知道如何处理它,那么您基本上就被卡住了。

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

    • 尝试仅使用(或:主要)低级类型或它们的数组来解决问题。
    • 问题通常很小 - 使用智能但复杂的算法并不总能让您获胜,尤其是如果不太智能的算法可以用仅使用(数组)低级类型的代码表示。以 nprofile!)
    • 考虑编写一个仅使用低级类型(通常大小小于 64 的问题实例)即可解决的特殊情况,即使您必须为较大的问题实例保留其他代码。
    • 学习按位算术以帮助您实现上述两个想法。
    • BitArray 可以成为您的朋友,与 Dictionary 相比,或者更糟的是 List。但请注意,实现不是最优的;你可以自己写一个更快的版本。除了测试您的参数是否超出范围等之外,您通常可以构建您的算法,以便索引无论如何都不会超出范围 - 但您不能从标准 BitArray 中删除检查,并且 它不是免费的。
    • 作为仅使用低级类型数组的示例,BitMatrix 是一个相当强大的结构,可以只是一个 ulong 数组,您甚至可以使用它来遍历它一个 ulong 作为“前”,因为您可以在恒定时间内取最低位(与广度优先搜索中的队列相比 - 但显然顺序不同,取决于项目的 index 而不是纯粹是你找到它们的顺序)。
    • 除法和取模真的很慢,除非右手边是常数。
    • 浮点数学不总体上不再比整数数学慢(不是“你可以做的事情”,而是“你可以跳过做的事情”)
    • 分支是不是免费的。如果您可以使用简单的算术(除除法或模数之外的任何运算)来避免它,您有时可以获得一些性能。将分支移到循环外几乎总是一个好主意。

    【讨论】:

    • 那里有一些对我有很大帮助的好东西 - 谢谢!
    【解决方案8】:

    人们对真正重要的事情有有趣的想法。 Stack Overflow 充满了诸如 ++i 比 i++ 更“高效”的问题。 Here's an example of real performance tuning,对于任何语言,这基本上都是相同的过程。如果代码只是以某种方式编写“因为它更快”,那就是猜测。

    当然,您不会故意编写愚蠢的代码,但如果猜测有效,则不需要分析器和分析技术。

    【讨论】:

      【解决方案9】:

      事实是,没有完美的优化代码。但是,您可以在已知 CPU 类型(和数量)、已知平台(Microsoft?Mono? )、已知的框架/BCL 版本、已知的 CLI 版本、已知的编译器版本(错误、规范更改、调整)、已知的总内存和可用内存量、已知的程序集来源(GAC?disk?remote ?),具有来自其他进程的已知后台系统活动。

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

      【讨论】:

        【解决方案10】:

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

        通过告诉系统你想做什么,它可以找出最好的方法。 LINQ 很好,因为它的结果在您需要它们之前不会被计算出来。如果您只使用第一个结果,则不必计算其余结果。

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

        【讨论】:

        • 讽刺 LINQ 增加了额外的香肠,人们应该想知道是否存在没有它的解决方案。
        【解决方案11】:

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2010-11-10
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多