.NET GC 暂停时间分析

.NET GC 暂停时间分析

过去的几个月里面,有几篇博客文章讨论了各个编程语言或者运行时机制GC的暂停时间。一切的起源都是源于一片研究Haskell GC延迟 的文章,之后又发布了一篇Haskell,OCaml 和 Racket 对比,之后就是Go GC的理论与实践, 最后还有一篇Erlang 的GC情况

在阅读了上面的文章之后,我突然想看一下 .NET GC与他们的对比情况。


上面的文章都使用了几乎相同的一个测试程序来查看GC的情况,测试程序是基于一个消息总线的实现场景编写的(具体信息)。近期Franck Jeannin 开始编写.NET版本的测试程序,我们这篇文章就是基于这个进行讨论的。

相关的测试代码:

for (var i = 0; i < msgCount; i++)
{
    var sw = Stopwatch.StartNew();
    pushMessage(array, i);
    sw.Stop();
    if (sw.Elapsed > worst)
    {
        worst = sw.Elapsed;
    }
}

private static unsafe void pushMessage(byte[][] array, int id)
{
    array[id % windowSize] = createMessage(id);               
}

所有的实现代码

这里我们创建了一个"message",这个message实际上就是一个byte[1024]的数组,然后把它放进了一个byte[][]的数据结构里面。这个过程被重复执行了1千万次(msgCount控制),但是array里面最多只能存20万个数据(windowSize控制),也就是我们每次会覆盖旧的数据。

这里统计了每次添加数据的时间,这个操作的时间理论上是很快的,所以统计的时间大概率会等于GC的暂停时间。当然我们也可以通过PerfView tool 来监控GC的暂停时间,双重监控能让我们的结果更具备权威性。

GC两种模式对比(Workstation 与 Server)


Java的GC有很多可配置项目, 但是 .NET GC的选项就很少了,只有如下的这几项:

  • 工作站模式(WorkStation)
  • 服务器模式(Server)
  • 并发模式或者单线程模式

我们这里只比较工作站模式和服务器模式的差异,为了保证数据的准确性,两个比较情况下,都开启并发模式,而且并发模式的暂停时间也会更短一些。

这里有一篇非常棒的概述文章,描述了GC在并发模式下,工作站和服务器模式的异同, 介绍了两种模式的一些针对性的优化内容:

WorkStation GC 是为桌面应用程序专门设计的,可以最大限度的减少GC中花费的时间。这种模式下,GC发生的频率更加频繁,但是暂停的时间相对更短。而服务器模式针对吞吐量做了优化,暂停时间略长,内存消耗也更大,每次触发的内存条件更高,可以支持服务器处理更多的数据。

因此,工作站模式应该比服务器模式的暂停时间更短,结果证明了这一点,具体的细节可以看下图,这个结果是通过HdrHistogram.NET 统计得出的。

image

X轴是对数结果,可以通过图片看到工作站模式(WKS)的暂停时间在99.99%的时候开始增加,而服务器模式(SVR)从99.9999%的时候开始增加。

另一种方式可以通过下面的表格分析结果,通过表格可以看出,虽然工作站模式的最大暂停时间比较短,但是频次更多,总体的暂停时间更长。

GC Mode Max GC Pause GC Pauses Total GC Pause Time Elapsed Time Peak Working Set (MB)
Workstation - 1 28.0 1,797 10,266.2 21,688.3 550.37
Workstation - 2 23.2 1,796 9,756.6 21,018.2 543.50
Workstation - 3 19.3 1,800 9,676.0 21,114.6 531.24
Server - 1 104.6 7 646.4 7,062.2 2,086.39
Server - 2 107.2 7 664.8 7,096.6 2,092.65
Server - 3 106.2 6 558.4 7,023.6 2,058.12

因此如果你关心如何减少最大暂停时间,那么工作站模式肯定是不二选择,但是总体上你需要更多的暂停时间,因此你的程序吞吐量会降低。此外,对于服务器模式,他为每个CPU分配了一个堆,所以容量更大。

很幸运的是,在.NET里面我们可以选择使用哪种模式(译者注:不能选的话还怎么玩), 这里有一篇讲现代垃圾回收机制很棒的文章(译者注:这篇文章我也翻译过,可以看这里) ,里面提到了Go的GC几乎只优化暂停时间。

事实上,Go GC并没有实现更多的新创意和新的研究成果。正如他们所公示的内容,他是一个很直接的并发标记/扫描收集的机制,这种方式还是基于1970年的研究想法。GC唯一不同的是他通过牺牲一些其他合理的性能来优化暂停的时间,但是GC的技术发言和推销材料上,却没有提及他们牺牲了什么内容,让那些不熟悉或者不关心垃圾回收机制的人忘记了这段牺牲的内容,甚至暗示出,Go语言的其他竞争对手(Java等)的垃圾回收很差。

不同数量级的存活对象情况下,最大的暂停时间分布

为了进一步研究这块内容,让我们来看一下最大暂停时间与活跃对象数量的变化情况。回头看一下我们的实例代码,我们仍然是分配10,000,000条信息(msgCount控制),然后通过改变windowSize来控制保存的数量。


image

可以清晰地看到,暂停时间与存活对象数量是成比例的(线性),为什么是这样的?,让我们通过PerfView帮我们分析一下。

windowSize 为 100,000,时间单位为毫秒

GC Index Gen Pause MSec Gen0 Alloc MB Peak MB After MB Promoted MB Gen 0 MB Gen 1 MB Gen 2 MB LOH MB
2 1N 39.443 1,516.354 1,516.354 108.647 104.831 0.000 107.200 0.031 1.415
3 0N 38.516 1,651.466 0.000 215.847 104.800 0.000 214.400 0.031 1.415
4 1N 42.732 1,693.908 1,909.754 108.647 104.800 0.000 107.200 0.031 1.415
5 0N 35.067 1,701.012 1,809.658 215.847 104.800 0.000 214.400 0.031 1.415
6 1N 54.424 1,727.380 1,943.226 108.647 104.800 0.000 107.200 0.031 1.415
7 0N 35.208 1,603.832 1,712.479 215.847 104.800 0.000 214.400 0.031 1.415

windowSize 为 400,000,时间单位为毫秒

GC Index Gen Pause MSec Gen0 Alloc MB Peak MB After MB Promoted MB Gen 0 MB Gen 1 MB Gen 2 MB LOH MB
2 0N 10.319 76.170 76.170 76.133 68.983 0.000 72.318 0.000 3.815
3 1N 47.192 666.089 0.000 708.556 419.231 0.000 704.016 0.725 3.815
4 0N 145.347 1,023.369 1,731.925 868.610 419.200 0.000 864.070 0.725 3.815
5 1N 190.736 1,278.314 2,146.923 433.340 419.200 0.000 428.800 0.725 3.815
6 0N 150.689 1,235.161 1,668.501 862.140 419.200 0.000 857.600 0.725 3.815
7 1N 214.465 1,493.290 2,355.430 433.340 419.200 0.000 428.800 0.725 3.815
8 0N 148.816 1,055.470 1,488.810 862.140 419.200 0.000 857.600 0.725 3.815
9 1N 225.881 1,543.345 2,405.485 433.340 419.200 0.000 428.800 0.725 3.815
10 0N 148.292 1,077.176 1,510.516 862.140 419.200 0.000 857.600 0.725 3.815
11 1N 225.917 1,610.319 2,472.459 433.340 419.200 0.000 428.800 0.725 3.815

额外堆

最后,如果你想在.NET里面彻底消除暂停时间,可以使用额外堆,通过unsafe的标记来实现:

var dest = array[id % windowSize];
IntPtr unmanagedPointer = Marshal.AllocHGlobal(dest.Length);
byte* bytePtr = (byte *) unmanagedPointer;

// Get the raw data into the bytePtr (byte *) 
// in reality this would come from elsewhere, e.g. a network packet
// but for the test we'll just cheat and populate it in a loop
for (int i = 0; i < dest.Length; ++i)
{
    *(bytePtr + i) = (byte)id;
}

// Copy the unmanaged byte array (byte*) into the managed one (byte[])
Marshal.Copy(unmanagedPointer, dest, 0, dest.Length);

Marshal.FreeHGlobal(unmanagedPointer);

注意:我不建议使用这种方式,除非你分析了是GC暂停时间的问题,他被成为不安全的方式是有原因的。


image

结果显而易见,是有效的。但是也没什么可奇怪的,我们无法管理GC,所以也没有GC的暂停时间。

在结束之前,我们再做最后看一下Maoni Stephens做的一些测试,Maoni Stepthens是.NET GC的核心开发成员。在他的一片博客中提到的:

查看个别的暂停时间最长的GC操作,不应该总是关注full GC,因为full GC是可以再并发模式下完成的。所以老年代的GC(G2)可能会比其他两代更短。即使个别情况下,老年代的full GC时间最长,但是也不一定非要关注他,因为他的频率很低,相反,其他两代的GC才是GC暂停时间的主要组成部分,如果暂停时间对你产生了影响,那也应该检查一下他们的情况。

Tips:.NET GC分为0,1,2代,这里的其他两代值得是0,1代,2代暂时成为老年代。

所以如果GC暂停时间对你的应用程序产生了影响,请理性的分析他。


原文链接:Analysing Pause times in the .NET GC
译者:JYSDeveloper

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335

推荐阅读更多精彩内容

  • 原文阅读 前言 这段时间懈怠了,罪过! 最近看到有同事也开始用上了微信公众号写博客了,挺好的~给他们点赞,这博客我...
    码农戏码阅读 5,927评论 2 31
  • 垃圾回收算法具体实现 翻译原文 => plumbr Java GC handbook 前文参见: Java垃圾回收...
    foxracle阅读 2,828评论 0 15
  • JVM架构 当一个程序启动之前,它的class会被类装载器装入方法区(Permanent区),执行引擎读取方法区的...
    cocohaifang阅读 1,622评论 0 7
  • 转载blog.csdn.net/ning109314/article/details/10411495/ JVM工...
    forever_smile阅读 5,338评论 1 56
  • [TOC] 内存管理 一、托管堆基础 在面向对象中,每个类型代表一种可使用的资源,要使用该资源,必须为代表资源的类...
    _秦同学_阅读 3,746评论 0 3