在谈到.NET性能调优时,我们有一个常被误解的概念:避免内存分配的重要性。人们相信,由于内存分配速度很快,因此对性能的影响很小。要了解导致这种误解的原因,我们必须回到COM编程时代,就像我们在C++和VisualBasic4到6中看到的那样。对于COM,内存是使用垃圾收集器的引用计数形式管理的。每当一个对象被分配给一个引用变量时,一个隐藏的计数器就会增加。如果变量被重新分配或超出范围,计数器将被取消。如果计数器达到0,则删除该对象,释放其他地方的内存。这个内存管理系统是“确定性的”。通过仔细分析,您可以确定对象何时被删除。这意味着您可以自动释放数据库连接等资源。使用.NET,您需要一个单独的机制(例如,销毁/启用)来确保及时释放非内存资源。引用计数垃圾收集器具有三个主要缺点。首先,它们容易受到“循环引用”的影响。如果两个对象互相引用,即使是间接引用,引用计数也不能减为0,就会导致内存泄漏。我们必须仔细编写代码以避免循环引用或提供某种解构方法来在不再需要对象时打破循环。在多线程环境中工作时会遇到另一个主要缺点。为了避免竞争条件,某种类型的锁定机制(例如:锁、增量、自旋锁等)需要确保重新计数仍然正确。这些操作非常昂贵。***,可用内存位置列表可能会变得支离破碎,在活动对象之间创建许多小的、不可用的空间。内存分配通常涉及遍历具有空闲空间的连续链表,以便找到足够大的位置来满足请求的对象。(内存碎片也存在于.NET中的“大对象堆”或“LOH”中。)相比之下,将内存分配给类似.NET或Java的“标记-清除”形式的垃圾收集器很容易。是一个简单的指针递增机制。分配并不比分配一个整数更昂贵。实际成本仅在GC实际运行时支付,这通常通过使用分代收集器来减轻。当.NET刚出现时,很多人抱怨说.NET的垃圾收集器的非确定性行为会损害性能并且难以解释。微软当时的反驳是,对于大多数用例,尽管间歇性GC暂停,“标记和清除”垃圾收集器实际上会更快。不幸的是,随着时间的推移,这条消息变得有点混乱。即使我们接受标记清除垃圾收集器比引用计数更快的理论,但这并不意味着它在绝对意义上是必要的。内存分配和相关的内存压力通常是难以检测的性能问题的原因。此外,使用的内存越多,CPU缓存的效率就越低。虽然主RAM非常大,以至于在大多数用例中很少使用基于磁盘的虚拟内存,但相比之下,CPU中的缓存很小。从RAM填充CPU缓存所需的时间可能需要数十甚至数百个CPU周期。在最近的一篇文章中,FransBouma确定了几种优化内存使用的技术。虽然他主要关注提高ORM性能,但这些建议在各种情况下都很有用。他的建议包括:避免使用参数数组parameter关键字很有用,但与普通函数调用相比代价高昂,因为它需要分配内存。API应该为常用参数计算提供无参数重载。您还应该提供IEnumerable或IList的重载,以便在调用函数之前不需要将集合复制到数组中。如果在定义后立即添加数据,则可以预定义数据结构的大小。列表或其他集合类可以在填充时多次调整大小。每个调整大小操作都会分配另一个内部数组并用前一个数组填充它。您可以通过向集合的构造函数提供容量参数来避免这种开销。成员的惰性初始化如果您知道大多数时候不需要给定的对象,那么您应该使用惰性初始化来避免过早地为其分配内存。通常这是手动完成的,因为Lazy类本身需要分配内存。早在2011年,我们就报道过Microsoft试图通过使用类似的技术来减少任务的大小。他们报告说创建任务所需的时间减少了49%到55%,所需空间减少了52%。
