Tips for optimizing C#/.NET programs
最近优化似乎是一门失传的艺术。难道没有一段时间所有的程序员都从他们的代码中榨取每一盎司的效率吗?经常在雪地里走五英里?
本着恢复丢失的艺术的精神,对于优化C/.NET代码的简单(或复杂)更改,您知道哪些技巧?因为这是一个如此广泛的事情,取决于一个人试图完成什么,这将有助于提供与您的提示上下文。例如:
- 当将多个字符串连接在一起时,使用
StringBuilder 。请参阅底部的链接了解有关此问题的注意事项。 - 使用
string.Compare 比较两个字符串,而不是执行string1.ToLower() == string2.ToLower() 之类的操作。
目前普遍的共识似乎是衡量的关键。这种做法忽略了一点:测量并不能告诉你出了什么问题,也不能告诉你遇到瓶颈时该怎么做。我曾经遇到过字符串连接瓶颈,不知道该怎么做,所以这些技巧很有用。
我的观点是,即使张贴这是有一个地方共同的瓶颈,以及如何避免它们之前,甚至遇到它们。它甚至不一定是任何人都应该盲目遵循的即插即用代码,而是更多地了解应该考虑性能,至少在某种程度上,还有一些常见的陷阱需要注意。
我可以看出,知道一个提示为什么有用以及应该在哪里应用可能是有用的。为了得到
It seems like optimization is a lost art these days.
比如说,有一天,显微镜的制造被当作一门艺术来实践。人们对光学原理了解甚少。零件没有标准化。管子、齿轮和镜片必须由熟练工人手工制作。
现在显微镜是作为一门工程学科生产的。物理的基本原理是非常清楚的,现成的零件是广泛的,显微镜建筑工程师可以做出明智的选择,如何最好地优化他们的仪器,以完成设计任务。
性能分析是一门"丢失的艺术",是一件非常非常好的事情。那是一门艺术。优化是一个可以通过仔细应用固体工程原理来解决的工程问题。
在过去的几年里,我被问了很多次,想知道我的"技巧和窍门"列表,人们可以用它来优化他们的vbscript/他们的jscript/他们的活动服务器页面/他们的vb/他们的c代码。我总是抵制这一切。强调"技巧和诀窍"正是接近绩效的错误方法。这样就产生了难以理解、难以推理、难以维护的代码,通常不会明显快于相应的简单代码。
正确处理性能的方法是将其作为工程问题处理,就像处理其他问题一样:
- 设定有意义的、可衡量的、以客户为中心的目标。
- 构建测试套件,在现实但可控且可重复的条件下,根据这些目标测试您的性能。
- 如果这些套件显示您没有达到目标,请使用诸如profiler之类的工具来找出原因。
- 优化探查器标识为性能最差的子系统的检查。对每一项更改进行分析,以便清楚地了解每一项更改对性能的影响。
- 重复,直到三件事中的一件发生(1)你达到了你的目标并发布了软件,(2)你把你的目标向下修改为你能达到的目标,或者(3)你的项目因为你不能达到你的目标而被取消。
这与解决任何其他工程问题一样,比如添加一个特性——为特性设置以客户为中心的目标,跟踪实现可靠的进度,通过仔细的调试分析解决问题,不断迭代直到发布或失败。性能是一个特性。
对复杂的现代系统进行性能分析需要有纪律性,并关注坚实的工程原理,而不是一袋仅适用于琐碎或不现实情况的技巧。我从来没有通过应用技巧和技巧解决过实际的性能问题。
找一个好的探查器。
在没有好的分析器的情况下,甚至不必费心优化C(实际上是任何代码)。实际上,手头上有一个采样和一个跟踪分析器有很大的帮助。
如果没有一个好的探查器,您很可能会创建错误的优化,最重要的是,优化一开始就不是性能问题的例程。
分析的前三个步骤应该始终是1)测量,2)测量,然后3)测量……
优化指南:
随着处理器速度不断加快,大多数应用程序的主要瓶颈不是CPU,而是带宽:片外存储器的带宽、磁盘的带宽和网络的带宽。
从远端开始:使用yslow了解为什么最终用户的网站速度较慢,然后向后移动并修复数据库访问,使其不太宽(列)也不太深(行)。
在极少数值得做任何事情来优化CPU使用的情况下,请注意不要对内存使用产生负面影响:我看到过"优化",开发人员试图使用内存来缓存结果以节省CPU周期。最终的效果是减少了缓存页面和数据库结果的可用内存,这使得应用程序的运行速度慢得多!(见测量规则。)
我也看到过这样的情况:一个"哑"的非优化算法击败了一个"聪明"的优化算法。永远不要低估优秀的编译器编写者和芯片设计人员如何将"低效"的循环代码转换成可以完全在片上内存中运行且具有流水线功能的超高效代码。你的"聪明"的基于树的算法,有一个打开的内部循环倒计时,你认为是"有效的",可以简单地击败,因为它在执行过程中未能留在芯片内存中。(见测量规则。)
使用ORM时,请注意N+1选择。
1 2 3 4 5 | List<Order> _orders = _repository.GetOrders(DateTime.Now); foreach(var order in _orders) { Print(order.Customer.Name); } |
如果客户不急于加载,这可能会导致多次访问数据库。
- 不要使用幻数,使用枚举
- 不要硬编码值
- 尽可能使用泛型,因为它是类型安全的,避免装箱和拆箱
- 在绝对需要的地方使用错误处理程序
- 处置,处置,处置。clrwind不知道如何关闭数据库连接,因此在使用和释放非托管资源后关闭它们
- 用常识!
如果您将一个方法识别为瓶颈,但您不知道如何处理它,那么您基本上就陷入了困境。
所以我会列出一些事情。所有这些东西都不是银弹,您仍然需要分析您的代码。我只是为你能做的事提出建议,有时也能帮上忙。尤其是前三个很重要。
- 尝试只使用(或:主要是)低级类型或它们的数组来解决问题。
- 问题往往很小——使用一个智能但复杂的算法并不总是能让你获胜,特别是如果不太智能的算法可以用只使用(数组)低级类型的代码来表示。例如,insertionsort vs mergesort for n<=100或tarjan的主宰查找算法vs使用位向量天真地解决了n<=100问题的数据流形式。(100当然只是给你一些想法-简介!)
- 考虑编写一个只使用低级类型(通常是大小小于64的问题实例)就可以解决的特殊情况,即使对于较大的问题实例必须保留其他代码。
- 学习位算术来帮助你处理上面的两个想法。
- 与字典相比,bitarray可以是您的朋友,或者更糟的是,列表。但是要注意实现不是最佳的;您可以自己编写一个更快的版本。与测试您的参数是否超出范围等不同,您通常可以构造您的算法,以便索引不会超出范围——但是您不能从标准位数组中删除检查,而且它也不是免费的。
- 作为一个只使用低级类型数组的例子,位矩阵是一个相当强大的结构,可以实现为一个ulong数组,甚至可以使用ulong作为"front"遍历它,因为可以在恒定时间内取最低阶位(与宽度优先搜索中的队列相比-但显然顺序是不同的,它取决于项目的索引,而不仅仅是您找到它们的顺序)。
- 除法和模是很慢的,除非右手边是常数。
- 浮点数学一般不再比整数数学慢(不是"你能做的事",而是"你能跳过的事")。
- 分支不自由。如果您可以使用一个简单的算术(除法或模以外的任何运算)来避免它,那么有时您可以获得一些性能。将分支移动到循环外部几乎总是一个好主意。
好的,我必须加入我最喜欢的:如果任务足够长,可以进行人与人之间的交互,请使用调试程序中的手动中断。
与事件探查器相比,这为您提供了一个调用堆栈和变量值,您可以使用这些值来真正了解正在发生的事情。
这样做10-20次,您就可以很好地了解什么样的优化可能真正起到作用。
人们对真正重要的事情有着有趣的想法。堆栈溢出充满了问题,例如,
当然,您不会故意编写愚蠢的代码,但如果猜测有效,就不需要配置文件和配置技术。
事实上,没有完美的优化代码。但是,您可以在已知的系统(或一组系统)上、已知的CPU类型(和计数)上、已知的平台(Microsoft)上,针对特定的代码部分进行优化。单?),一个已知的框架/bcl版本,一个已知的cli版本,一个已知的编译器版本(错误、规范更改、调整),一个已知总量和可用内存,一个已知的程序集源(gac?磁盘?远程?),具有来自其他进程的已知后台系统活动。
在现实世界中,使用分析器,并查看重要的部分;通常明显的事情是涉及I/O的任何事情,涉及线程的任何事情(同样,版本之间的变化很大),以及涉及循环和查找的任何事情,但是您可能会惊讶于"明显不好"的代码实际上不是问题,以及"明显好"的代码是什么。奥德是个罪魁祸首。
告诉编译器该做什么,而不是怎么做。例如,
通过告诉系统你想做什么,它可以找出最好的方法来做。Linq很好,因为它的结果只有在需要时才会计算出来。如果只使用第一个结果,就不必计算其余的结果。
最终(这适用于所有编程)最小化循环,最小化循环中的操作。更重要的是尽量减少循环中的循环数。O(n)算法和O(n^2)算法有什么区别?O(n^2)算法在循环内有一个循环。
我并没有真正尝试优化我的代码,但有时我会通过使用类似于Reflector的东西将程序放回源代码。然后将我的错误与反射镜的输出进行比较是很有趣的。有时我发现我所做的更复杂的形式是简化的。也许不能优化事情,但可以帮助我找到更简单的问题解决方案。