当前位置:首页 > .NET > C#

[通译] 编写高性能 .NET 代码-第二章 GC - 减少分配率, 最重要的规则,缩短对象的生命周期,减少对象层次的深度,减少对象之间的引用,避免钉住对象(Pinning)

优良自学吧提供[通译] 编写高性能 .NET 代码-第二章 GC - 减少分配率, 最重要的规则,缩短对象的生命周期,减少对象层次的深度,减少对象之间的引用,避免钉住对象(Pinning),[翻译] 编写高性能 .NET 代码--第二章 GC -- 减少分配率, 最重要的规则,缩短对象的生命周期,减少对象层次的深度,减少对象之间的引

[翻译] 编写高性能 .NET 代码--第二章 GC -- 减少分配率, 最重要的规则,缩短对象的生命周期,减少对象层次的深度,减少对象之间的引用,避免钉住对象(Pinning)

减少分配率

这个几乎不用解释,减少了内存的使用量,自然就减少GC回收时的压力,同时降低了内存碎片与CPU的使用量。你可以用一些方法来达到这一目的,但它可能会与其它设计相冲突。

你需要在设计对象时仔细检查每个它并问自己:

  1. 我真的需要这个对象吗?
  2. 这个字段是我需要的吗?
  3. 我能减少数组的尺寸吗?
  4. 我能缩小primitives的尺寸吗(用Int32替换Int64,其它)?
  5. 这些对象,是否只有在极少数情况下,或者只有初始化的时候才用到?
  6. 是否能将一些类转为结构体使他们在栈上分配或者成为某个对象的一部分?
  7. 我是否分配了大量内存,但实际只使用其中很小的一部分?
  8. 我可以从其它地方拿到相关数据?

小故事:在服务端一个响应请求的函数里,我们发现在一次请求里会分配一些比内存段要大的内存。这样导致每次请求我们都会触发一次完整的GC,这是因为CLR要求所有的0代对象都在一个内存段里,当前分配的内存段满了,就会开辟一个新的内存段,同时对原先的内存段做一次2代的回收。这不是一个好的实现,因为我们除了减少内存分配外别无它法。

最重要的规则

对于垃圾回收的高性能编程有一个基本规则,事实上也是代码设计的指导规则。

要收集的对象要么在0代,要么不存在
Collect objects in gen 0 or not at all.

不同的是,你希望一个对象拥有极短的生命周期,在GC的时候永远不要碰到它,或者,如果你做不到这一点,它们应该去2代,尽可能的快,永远的呆在那,永远不会被回收。这意味着你永远保持对长生命周期对象的引用。通常,也意味着对象可重复使用,尤其是在大对象堆里的对象。
GC每高一个世代的回收会比上一个世代更加耗时。如果你想保持许多0,1代和少量的2代对象。即使开启后台GC做2代做回收,也会消耗相当CPU运算量,你可能更愿意将这部分的CPU消耗给应用程序,而不是GC。

Note 你可能听过一个说法,每10次0代的回收会产生一次1代的回收,每10次1代的回收会产生1次2代的回收。这其实是不正确的,但是你要明白,你要尽可能产生多次快速的0代回收,以及少量的2代回收。

你最好避免进行1代回收,主要是因为已经从0代提升到1代的对象,会在这时候被转入2代。1代是对象进入2代的一个缓冲区。
理想情况下,你分配的每一个对象应该在下一次0代回收前结束生命周期。你可以测量两次GC的时间间隔,并将其与应用程序里对象的生命周期长度做对比。有关如何使用工具测量生命周期的信息,可以在本章结尾看到。
你可能不习惯这样思考,但这规则切入了应用程序的方方面面,你需要经常思考它,在心态要做根本的转变,这样才能实现这个最重要的规则。

缩短对象的生命周期

一个对象的作用范围越短,在下一个GC出现时,它被提升到下一代的机会就越小。一般来说,在你需要之前,不要创建对象。
同时,当对象创建的代价如此之高时,异常就可以在较早的时候创建,这样不会干扰到其他处理逻辑。
另外,你还要确保对象尽可能早的退出作用域。对于局部变量,你可以在最后一次使用后,甚至在方法结束前将其生命周期结束。你可一个用{}将代码包括起来,这不会对你的运行产生影响,但编译器会认为在这个范围的对象已经完成了他的生命周期,不再被使用了。如果需要调用对象的方法,尽量减少第一次和最后一次的时间间隔,以便GC尽早的回收对象。
如果对象关联(引用)了一些会长时间保持的对象,则需要解除他们的引用关系。你可能会有更多的空值检查(null判断),这可能会让代码变得更复杂。也会在对象的可用状态(always having full state available)上与效率之间造成紧张关系,特别是调试的时候。
解决的一种方法是,将要清空的对象转换为另外一种方式存在,例如:日志消息,这样在后面的调试时可以查询到相关信息。
另外一种方法是为代码增加可配置选项(不解除对象之间的关系):运行程序(或者运行程序里特定的一个部分,例如一个特定的请求),在这个模式中没有解除对象引用关系,而是尽可能让对象一直保持方便调试。

减少对象层次的深度

如本章开头所述,GC在回收时会顺着对象的引用关系进行遍历。在服务器GC模式,GC会以多线程方式运行,但如果一个线程需要处理一个对象层次很深,则所有已经处理完的线程都需要等这个线程完成处理后才能退出。在今后的CLR版本里,你可以不用太关注这个问题,GC在多线程执行时会采用更好的标记算法做负载均衡。但如果你对象层次很深,这个问题还是要关注一下的。

减少对象之间的引用

这与上节的深度有关,但也有一些其它的因素。
一个对象如果引用了很多对象(数组,List吧),那它将花很多时间在遍历对象上。是GC造成长时间的一个问题,因为它有一个复杂的关系图。
另外一个问题是,如果无法轻松的确定对象有多少引用关系,那么你就无法准确的预测对象的生命周期。减少这种复杂度是相当有必要的,它不但可以让代码更健壮,同时也方便调试以及获得更好的性能。
另外,还要注意不同世代对象之间的引用也会导致GC的效率低下,特别是旧对象对新对象的引用。例如,如果2代对象在0代对象里有引用关系,那么每次发生0代的GC时,也需要扫描部分2代对象,看看他们是否仍然保持到0代对象的引用上。虽然这不是一次完整的GC,但它仍然是不要的工作,你应该尽量避免这种情况。

避免钉住对象(Pinning)

钉住对象可以保证从托管代码往本地代码里传递数据的安全。常见的有数组和字符串。如果你的代码不需要与本地代码做交互,则不用考虑它的性能开销。
钉住对象就是让对象在垃圾回收(压缩阶段)时无法移动他。虽然钉住对象不会造成多大开销,但它会妨碍到GC的回收操作,增加内存碎片的可能性。GC在回收时会记录对象的位置,以便在重修分配时利用它们之间的空间,但如果钉住的对象很多,会导致内存碎片的增加。
钉可以是显示的也可以使隐式的。显示的是使用GCHandle时用GCHandleType.Pinned参数进行设置,或者在unsafe模式下使用 fixed 关键字。使用fixed关键字和GCHandle的差别在于是否会显示调用Dispose方法。使用fixed虽然很方便,但是不能在异步情况下使用,但还是可以创建一个句柄对象(GCHandle),在回调时传回并处理。
隐式的钉住对象则比较常见,但也更难排查,也更难移除。最明显的例子就是通过平台调用(P/Invoke)将对象传递给非托管代码。这不仅仅是你的代码—--你经常调用的一些托管API,实际上也是会调用本地代码,也会将对象钉住。
CLR也会将自己的一些数据给钉住,但这通常不需要你来关心。
理想情况下,你应该尽可能的不要钉住对象。如果不能做到,那么遵循之前的重要规则,尽可能让这些被钉的对象尽早释放。如果对象只是简单的被钉住后释放,那么也不会有多少机会影响回收操作。你同时也要避免同时钉住很多个对象。被钉的对象被交换到2代或者在LOH里分配会稍微好些。根据这个规则,你可以在大对象堆上分配一个大的缓冲区,并根据实际需自己对缓冲区做管理。或者在小对象对上分配缓冲区,然后在钉住他们前,使他们升级到2代。这样比你直接将对象钉在0代上要好。


(本文来自互联网,不代表搜站(http://www.ylzx8.cn/)的观点和立场)
本站所有内容来自互联网,若本站收录的信息无意侵犯了贵司版权,请给我们来信(ylzx8cn@163.com),我们会及时处理和回复,谢谢