首页 文章 精选 留言 我的

精选列表

搜索[分区技术],共10000篇文章
优秀的个人博客,低调大师

使用 dotMemory 优化 dotMemory | 技术解析

dotMemory1是 JetBrains 推出的一款 .NET 内存分析器。我要讲一个经典的内部测试故事,在故事里我们用自己的工具 dotMemory 和dotTrace2 优化了 dotMemory 的一种算法。我们还使用 dotTrace 对其进行了更多改进,并使用 BenchmarkDotNet3完成了优化过程。 最开始,一位同事在 Slack 中给我发送消息,告诉我他在 dotMemory 的支配树4上遇到了问题。树的数据计算时间太长了,他实在等不到进程结束。好在问题是内部的,我们收集内存快照并展开了调查。 在本地机器上重现问题时,我们发现内存使用以每秒 1.17 GB 的速度快速增长,迫使进程开始使用交换文件。发生这种情况通常就意味着无法在合理的时间范围内收到结果。接下来,该用 dotMemory 解决 dotMemory 中的问题了。 进程完全耗尽物理内存后,快照会难以捕获,甚至操作系统也不再稳定。因此,我们决定在内存使用开始快速增长时捕获快照,但物理内存还有一些剩余。dotMemory 控制台分析器5是完成这项工作的最佳工具: 此命令以分析模式启动dotMemory.UI.64.exe。它会在“private bytes”量达到 20 GB 时立即捕获快照,并在分析完成后在 dotMemory 中打开快照。在我们的情况中,我们不得不手动停止分析(否则我们最终会再次交换)。我们一直等到数据足够,然后按 Ctrl+C。 现在有了快照,我们在 dotMemory 中将其打开。首先,分析支配树 – 我的同事无法获得的支配树,我在快照中得到了: 内存中充斥着大约 6000 万个CompactDominatorTreeNode+Builder对象,每个对象都包含一个字典。显然,对于大多数字典来说,大小 < 容量,这意味着大量内存被浪费了。 我们必须看看这些字典的用途,也许我们不需要这么多?我选择了 CompactDominatorTreeNode+Builder节点并导航到它的声明(使用上下文菜单或按 Ctrl+L)。这将我导航到我的 IDE (Rider6) 并打开了相应的类。运行 Find Usages7(查找用法)将我带到了CompactDominatorTree.Build方法,其中包含支配树压缩算法。 介绍一点背景:首先,dotMemory 构建一个支配树,如下所示。对于每个对象,它会搜索专门保留它的对象并保存该保留对象,如果没有,则保存表示缺少此类保留对象的标记。树可能会变得非常大,没有必要“按原样”向用户展示。因此,dotMemory 在树的每一级按对象类型对树节点进行分组。CompactDominatorTreeNode+Builder表示正在构建的紧凑(压缩)树的节点,字典用于按类型对节点进行分组。 算法以“下级→上级”格式接收树,每个对象都与其支配上级匹配。它按顺序遍历快照中的所有对象。每有一个支配上级尚未被处理的对象,紧凑树中就会创建一个新的组节点。当前节点成为组节点中的下级节点(现有节点或新创建的节点)。 使用这种方法,树以任意顺序遍历,每个节点都必须存储一个字典,字典按类型对节点进行分组。在大多数情况下,这都不是问题,因为紧凑树比包含所有对象的树要小得多,并且没有显著的内存过度使用。但是这个快照属于极端情况,因为即使是紧凑的树也很大。 现在的问题是,“我们怎样才能使用更少的字典?”,如果我们只有一个带组合键的字典,该怎么办?这将迫使我们必须执行分解,再次导致内存过度使用或更复杂的计算。我们不得不考虑其他方案。 我们退后一步,更全面地分析问题。使用不同的方式遍历树如何?比如广度优先搜索?这种方式看起来很可行。一个具有常规键的字典就足够了。这样,每当我们构建完成紧凑树的新级别,我们都会清除字典,从而可以在下一次迭代中重用它。 不过注意:“下级→上级”格式不适合广度优先搜索,因为检索给定节点的下级节点的计算开销过大。我们需要不同的输入树格式。好在我们已经有了一个备选方案:dotMemory 也能将支配树存储为“上级→下级”邻接列表。这意味着,我们可以在不增加计算复杂性的情况下更改算法以使用广度优先搜索。 我们进行了相应编辑并运行。我们看到内存使用的增长速度比以前慢得多,从每秒 1.17 GB 变为仅仅每秒 7 MB,但该进程仍然未能完成而且再次开始使用交换文件。 我们回到绘图板。我们捕获了另一个快照并将其打开。新的支配树看起来像这样: 这次仅紧凑树就占用了 16 GB。这个大小合理吗?也许我们还可以用更经济的方式存储这棵树? 我们按类型对节点进行分组以查找我们有多少个节点: 在 16 GB 的总大小中,树的 9000 万个节点仅占用 6 GB。这有些可疑。我们导航到 CompactDominatorTree2+Node声明的代码: publicsealedclassNode{publicDfsNumberDfsNumber{get;}//8bytespublicTypeIdObjectsType{get;}//4bytespublicintObjectsCount{get;}//4bytespublicintRetainedObjectsCount{get;}//4bytespubliculongRetainedBytes{get;}//8bytespublicboolIsContainedInSet{get;}//1bytepublicNodeParent{get;}//8bytesinternalJetArray<Node>ChildrenImpl{get;}//8bytes//wehaveomittedunnecessarydetails} 这里的有效负载只有 45 字节,但加上标题和对齐,一个 Node 对象就占用了 72 字节。它保留的更多,平均 192 字节。为了查看详细信息,我们切换到 Instances(实例)选项卡,使用查询CompactDominatorTree2+Node !g !a筛选内容(!g排除泛型,!a排除数组)。这是一个随机实例: 实例详情: 这可是好些开销。 现在,我们可以清楚地看到JetMutableArray并不是为大型树存储下级节点的最有效方式。我们想到的第一个解决方案当然是优化下级节点的存储。例如,我们可以将它们存储在标准数组中。但我们选择了一条更激进的道路。 大型数据结构在 dotMemory 中很常见。例如,对象图被存储为邻接列表,使用两个通过整数数组索引相互引用的结构数组。这很有趣,因为在 dotMemory 中,我们刚刚停止使用托管数组而转移到我们自己的基于内存映射文件的实现。这有助于我们在访问元素时实现最小开销(C 中的原生内存索引形式),以及加载和保存数组的快速方式(完全在操作系统级别),并且在序列化/反序列化时没有开销。另外,即使没有我们的参与,这些数组的片段也可以从内存中卸载,帮助我们在顺序遍历数组时释放物理内存。 我们决定在紧凑支配树上使用相同的方式。我们还将IsContainedInSet特性变成了一个位标志,并消除了没有使用的Parent属性。最后,我们为树节点得到了以下结构: [StructLayout(LayoutKind.Sequential,Pack=4)]publicreadonlystructNode{publicreadonlyTypeIdObjectsType;privatereadonlyuint_objectsCount;publicintObjectsCount=>(int)(_objectsCount&0x7FFFFFFF);publicboolIsContainedInSet=>(_objectsCount&0x80000000)!=0;publicreadonlyintRetainedObjectsCount;publicreadonlyulongRetainedBytes;publicreadonlyDfsNumberDfsEnter;publicreadonlyRange<uint>Children; 值得注意的是,通过这种顺序表示,我们甚至可以只使用一个指向下级节点的整数链接。不过,我们使用了一个范围,该范围允许我们按RetainedBytes升序对构建树的下级节点进行排序以帮助我们构建旭日图。对于上面显示的表示,节点 #1、#2 和 #3 的顺序可能会改变。 我们屏住呼吸,运行了程序。成了!内存使用峰值仅为 12 GB。支配树成功构建,耗时 55 分钟 – 仍然比我们期望的要长,但可以接受。快照现在总共包含 2.75 亿个对象,紧凑树包含 2 亿个节点。 这显然是一个好结果。我们一开始无法将大小降至 32 GB 以下,计算需要很长时间,现在,它已经降到 12 GB 并能在 55 分钟内就绪。然而,我们还是感觉我们可以做得更好。55 分钟绝对算不上理想。我们决定查明这些时间都用在了哪里。进入 dotTrace: 好在我们不需要等待进程完成,记录几分钟的性能就足够了。我们使用--timeout=3m键在 3 分钟后自动停止分析。我们打开快照,应用筛选以识别正在执行我们任务的线程,这是长时间保持完全加载的线程。调用树是这样的: 令人震惊的是,92% 的时间显然都被Dictionary.Clear占用了!我们简直不敢相信。Clear在标准库字典上怎么会表现得如此糟糕?原来,问题是字典在算法的第一次迭代中已经膨胀到了 22K 的巨大元素,而随后的迭代通常只涉及几十个元素。Dictionary.Clear的计算复杂度与Capacity(不是Count)成正比,特别是buckets.Length,它与Capacity成线性关系。这意味着我们在不断清除空元素!事实证明,选择清除和重用字典时,我们对模型进行了过多的优化。 再退一步,我们现在尝试了最直接的方式 – 为每次迭代创建一个新字典。我们再次启动秒表,运行代码,这次充满期待。哇!1 分 46 秒!垃圾回收器上的负载增加了,但它分布均匀并且管理良好,我们使用 dotTrace 验证了这一事实。就是这样。我们在不到 2 分钟的时间内处理了 2.75 亿个对象,内存使用峰值仅为 12 GB。我们相当高兴,准备好将优化的代码提交到仓库。 后来,我们决定再做一次测试。我们在不同的快照上运行了旧算法和新算法,比较性能。我们找到了一个大小相同但拓扑不同的快照,旧算法也可以处理,然后把它馈送给两种算法。令我们大吃一惊的是,新算法比旧算法慢了 33%,尽管计算复杂度没有变化。但这就是另一个故事了,我们以后再讲。如果您有兴趣,请在下方留言或直接联系我们! 相关链接: dotMemory:https://www.jetbrains.com.cn/dotmemory/ dotTrace:https://www.jetbrains.com.cn/profiler/ BenchmarkDotNet:https://github.com/dotnet/BenchmarkDotNet 支配树:https://www.jetbrains.com/help/dotmemory/Retained_by.html dotMemory 控制台分析器:https://www.jetbrains.com.cn/dotmemory/download/#section=commandline Rider:https://www.jetbrains.com.cn/rider/ Find Usages:https://www.jetbrains.com/help/rider/Navigation_and_Search__Finding_Usages.html 本博文英文原作者:Ilya Ivanov 关于 dotMemory .NET 内存分析器 用于优化 .NET 应用程序的内存使用,检测内存泄漏和克服各类内存问题。 扫码查看详情页开启30天免费试用 »»» ⏬ 戳「阅读原文」了解更多 本文分享自微信公众号 - JetBrains(JetBrainsChina)。如有侵权,请联系 support@oschina.cn 删除。本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册