在我的上篇文章[1](属于“ 编写高性能C#和.NET代码[2] ”系列文章的一部分)中,我们研究了如何开始解释dotMemory性能分析会话中的某些数据。
在这篇文章中,我们将继续调查为什么我们看到在出现第一个快照后大对象堆(LOH)大小增长了大约200ms,从而继续进行分析。
在时间线图中,紫色区域代表LOH的大小。它达到401.2 KB的大小,然后在其余的分析会话中保持不变。
我的性能分析会话包括两个快照。第二个快照发生在已分析的代码完成加载和解析75个文件之后。
要开始调查,我们将单击“快照2”的标题。
这将加载一个分析窗口。我们的目标是专门研究大型对象堆,因此我们可以打开“ Generations”选项卡。
在这里,我们可以查看每个堆生成的大小。LOH的大小为410,808字节,由4个对象组成。我们将双击“大型对象堆”以挖掘细节。
现在,我们可以在其中查看大对象堆中分配的类型。在此视图中,大部分堆都被CloudFrontRecordStruct数组占用。我们将双击该项目以继续调查。
现在,我们可以看到对该类型的传出引用。一个可容纳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字节。
在此示例中,我知道我的测试文件恰好包含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