对.NET 的性能调优来说,我们有一个普遍被误解的观念:规避内存分配的重要性。人们认为,由于内存分配是快速的,因此很少会对性能产生影响。
要理解导致这种误解的原因,我们必须回到在 C++ 和 Visual Basic 4 到 6 中所看到的 COM 编程时代。对于 COM,内存是使用引用计数形式的垃圾回收器进行管理的。每当将一个对象分配给一个引用变量时,就会增加一个隐藏的计数器。如果变量被重新分配或从作用域退出,计数器就会被取消。如果计数器达到 0,对象就会被删除,将内存释放到其他地方。
这种内存管理系统是“确定的”。通过仔细分析,你可以确定何时删除一个对象。这意味着你可以自动释放数据库连接等资源。而对于.NET 而言,你需要一个单独的机制 (例如,销毁 / 启用) 以确保非内存资源能够及时地被释放。
引用计数垃圾收集器有三个主要的缺点。首先,它们容易受到“循环引用”的影响。如果两个对象相互引用,即使是间接的,那么引用计数也不可能降为 0,这便会导致内存泄漏的发生。我们必须小心地编写代码,要么避免循环引用,要么提供某种解构方法以便在当对象不再需要时中断循环。
工作在多线程环境中时会遇到另一个主要的缺点。为了避免竞态条件,某种类型的锁机制 (例如: 锁住、增量、旋锁等) 需要确保重新计数仍然是正确的。这些操作出奇的昂贵。
最后,可用内存位置的列表可能会变成碎片化的,在活动对象之间会产生许多小的、不可用的空间。内存分配通常涉及到遍历一个有空闲空间的连续链表,以便寻找到一个足够大的位置来满足需求对象。(内存碎片在.NET 中也存在于“大对象堆”或“LOH”。)
相比之下,像.NET 或 Java 那样将内存分配到一个“标记 - 清扫”形式的垃圾回收器中,便是一个简单的指针增量机制。赋值并不比分配一个整数更昂贵。只有当 GC 实际运行时,才会支付实际成本,而且通常通过使用分代收集器来缓解这种情况。
当.NET 刚出现的时候,许多人抱怨.NET 的垃圾回收器不确定性的表现将会损害性能并且难以解释。当时微软的反驳是,对于大多数用例来说,尽管间歇的 GC 会暂停,但“标记 - 清扫”的垃圾回收器实际上会更快。
不幸的是,随着时间的推移,这条信息变得有些混乱。即使我们接受这样一种理论,即“标记 - 清扫”的垃圾回收器速度比引用计数更快,但这并不意味着它在绝对意义上是必须的。内存分配和相关的内存压力通常是很难检测性能问题的原因。
而且,使用的内存越多,CPU 缓存的效率就越低。虽然主 RAM 很大,以至于在大多数用例中几乎不会使用到基于磁盘的虚拟内存,但是相比之下,CPU 中的缓存是很小的。从 RAM 中填充 CPU 缓存所需的时间可能会占用数十甚至数百个 CPU 周期。
在最近的一篇文章中,Frans Bouma 确定了几种优化内存使用的技术。虽然他着重关注改善ORM 性能,但这些建议在各种情况下都很有用。他的这些建议包括:
避免参数数组
参数关键字是有用处的,但与普通的函数调用相比要昂贵,因为它需要内存分配。API 应该为常用的参数计算提供无参数重载。
还应该提供一个IEnumerable
如果定义之后立即添加数据,可先预定义数据结构的大小
List
惰性的初始化成员
如果你知道一个给定对象在大多数情况下是不需要的,那么你应该使用延迟初始化来避免过早地分配内存给它。通常这是手动完成的,因为 Lazy
早在 2011 年,我们就曾报道过微软试图通过使用类似技术来减少任务的规模。他们的报告显示,在创建一个
查看英文原文: Improving .NET Performance by Reducing Memory Usage
感谢冬雨对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ , @丁晓昀),微信(微信号: InfoQChina )关注我们。
评论