打开分析 查看堆快照文件_如何编写高性能的C#代码(七)浅析大对象堆

在我的上篇文章[1](属于“ 编写高性能C#和.NET代码[2] ”系列文章的一部分)中,我们研究了如何开始解释dotMemory性能分析会话中的某些数据。

在这篇文章中,我们将继续调查为什么我们看到在出现第一个快照后大对象堆(LOH)大小增长了大约200ms,从而继续进行分析。

94d6ee4beb226574fbcd14e85cd10291.png
图片

在时间线图中,紫色区域代表LOH的大小。它达到401.2 KB的大小,然后在其余的分析会话中保持不变。

我的性能分析会话包括两个快照。第二个快照发生在已分析的代码完成加载和解析75个文件之后。

要开始调查,我们将单击“快照2”的标题。

这将加载一个分析窗口。我们的目标是专门研究大型对象堆,因此我们可以打开“ Generations”选项卡。

bd41ced3fb31a96e1b34c2c9b24cd75b.png
图片

在这里,我们可以查看每个堆生成的大小。LOH的大小为410,808字节,由4个对象组成。我们将双击“大型对象堆”以挖掘细节。

e248eb8800e030751b9e210c16674403.png
图片

现在,我们可以在其中查看大对象堆中分配的类型。在此视图中,大部分堆都被CloudFrontRecordStruct数组占用。我们将双击该项目以继续调查。

0209553f8cf8da7df2430a54625004a8.png
图片

现在,我们可以看到对该类型的传出引用。一个可容纳16,384个CloudFrontRecordStruct元素的数组。这是哪里来的?

被分析的应用程序使用ArrayPool 租用一个传递到ParseAsync方法中的数组。

 var pool = ArrayPool<CloudFrontRecordStruct>.Shared;    for (var i = 0; i < 75; i++)    {        var newData = pool.Rent(10000);        try        {            await CloudFrontParserNew.ParseAsync(filePath, newData);        }        finally        {            pool.Return(newData, clearArray: true);        }    }

在此代码中,在概要分析条件下,我们正在模拟75个文件的处理。每一项都按顺序进行处理,因此我们在任何时候都只会使用池中的一项。在实际情况中,可能会发生并行文件处理。对于此示例,我们可以创建一个数组并简单地将其重用于每个文件,而不是从池中租用它。现在我们知道什么实例正在占用LOH内存的大部分。让我们继续考虑这个对象。为什么在大型对象堆中使用它,为什么占其393,240字节的大小?

让我们从查看CloudFrontRecordStruct定义开始。

 public struct CloudFrontRecordStruct    {        public string Date { get; set; }        public string Time { get; set; }        public string UserAgent { get; set; }    }

它具有三个字符串属性,这些属性将保存我们从CloudFront日志文件中解析的数据。结构没有开销,因此可以根据其成员的大小来计算其大小。在这种情况下,该结构保留对堆上字符串的三个引用。在我的64位计算机上,对堆上对象的引用为64位或8字节大小。

因此,每个CloudFrontRecordStruct需要3 x 8字节=总共需要24字节。

cf11fc7a0d9653f52fd46326f9093ea5.png
图片

在此示例中,我知道我的测试文件恰好包含10,000个项目,因此在从池中租用数组时,我要求使用10,000个元素的长度作为参数。

从dotMemory中我们可以清楚地看到,我们从池中提供的数组的容量为16,384个元素。那么为什么它大于我们要求的10,000?

我在代码中使用的共享ArrayPool通过将数组合并到某些大小的存储桶中来工作。共享阵列池是TlsOverPerCoreLockedStacksArrayPool的实例。该池有17个存储桶,从一个存储16个元素的存储桶开始,然后每个存储桶以2的幂递增。16、32、64、128等。最大数组大小为1,048,576个元素。

在此示例中,我定义了有10,000个元素的数组。可以提供合适数组的最小存储桶大小是包含16384个元素的数组的存储桶;2 ^ 14。

因此,我们的数组可以容纳16384个元素,现在我们可以通过将结构的已知大小乘以容量来计算其实际大小。每项16,384 x 24字节总计‭393,216‬字节。

这样就剩下24个字节了!数组是引用类型,在.NET中,引用类型有一些开销。数组还需要存储其长度。在我的64位系统上,全部花费是24字节。393,216字节+ 24字节= 393,240,这正是dotMemory报告的大小。

因此,我们现在了解了为什么在快照数据中看到了该数组。最后一个问题是为什么要在大对象堆中使用它?这很简单!大于85,000字节的任何对象都在大对象堆中分配。我们远远超过该限制,因此该实例占用的内存立即从LOH中分配。

由于我们已从共享的ArrayPool中应用了该阵列,因此它将在应用程序的生命周期内有效。该代码可以继续重用池中的同一实例。同样,在此示例中,合并有点多余。在实际情况中,池化对于并行处理可能具有更积极的好处。即使这样,它仍取决于该应用程序的编写方式,并应进行概要分析和基准测试。

摘要

在本文中,我们学习了一些其他的技术,可在对应用程序进行性能分析后深入研究dotMemory中可用的信息。借助数据,我们已经能够解释和理解大对象堆中分配的原因。根据我们对这个应用程序的了解,我们可以看到此分配是合理的,而不是与之相关的事情。

References

[1] 上篇文章: https://www.stevejgordon.co.uk/interpreting-the-dotnet-core-memory-timeline-in-jetbrains-dotmemory[2] 编写高性能C#和.NET代码: https://www.stevejgordon.co.uk/writing-high-performance-csharp-and-dotnet-code

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值