内存优化:Boxing
dotMemory
如今,许多开发人员都熟悉性能分析的工作流程:在分析器下运行应用程序,测量方法的执行时间,识别占用时间较多的方法,并致力于优化它们。然而,这种情况并没有涵盖到一个重要的性能指标:应用程序多次GC所分配的时间。当然,你可以评估GC所需的总时间,但是它从哪里来,如何减少呢? “普通”性能分析不会给你任何线索。
垃圾收集总是由高内存流量引起的:分配的内存越多,需要收集的内存就越多。众所周知,内存流量优化应该在内存分析器的帮助下完成。它允许你确定对象是如何分配和收集的,以及这些分配背后保留了哪些方法。理论上看起来很简单,对吧?然而,在实践中,许多开发人员最终都会这样说:“好吧,我的应用程序中的一些流量是由一些系统类生成的,这些系统类的名称是我一生中第一次看到的。我想这可能是因为一些糟糕的代码设计。现在我该怎么做?”
这就是这篇文章的主题。实际上,这将是一系列文章,我将在其中分享我的内存流量分析经验:我认为什么是“糟糕的代码设计”,如何在内存中找到其踪迹,当然还有我认为的最佳实践。
简单的例子:如果您在堆中看到值类型的对象,那么装箱肯定是罪魁祸首。装箱总是意味着额外的内存分配,因此移除它很可能会让您的应用程序变得更好。
该系列的第一篇文章将重点关注装箱。如果检测到“bad memory pattern”,该去哪里查找以及如何采取行动?
本系列中描述的最佳实践使我们能够将 .NET 产品中某些算法的性能提高 20%-50%。
您需要什么工具
在我们进一步讨论之前,先看看我们需要的工具。我们在 JetBrains 使用的工具列表非常简短:
- dotMemory 内存分析器。无论您试图查找什么问题,分析算法始终相同:
- 在启用内存流量收集的情况下开始分析您的应用程序。
- 在您感兴趣的方法或功能完成工作后收集内存快照。
- 打开快照并选择内存流量视图。
- Heap Allocations Viewer插件。该插件会突出显示代码中分配内存的所有位置。这不是必须的,但它使编码更加方便,并且在某种意义上“迫使”您避免过度分配。
Boxing
装箱是将值类型转换为引用类型。 例如:
int i = 5;
object o = i; // 发生装箱
为什么这是个问题?值类型存储在栈中,而引用类型存储在托管堆中。因此,要将整数值分配给对象,CLR 必须从栈中取出该值并将其复制到堆中。当然,这种移动会影响应用程序的性能。
一个对象的至少占用3个指针单元:对象头(object header)、方法表指针(method table ref)、预留单元(首字段地址/数组长度)
在x64系统3个指针单元意味24字节的开销,而一个int类型本身只占用4字节,其次,栈内存的由执行线程方法栈管理,方法内声明的local变量、字面量更是能够在IL编译期就预算出栈容量,效率远高于运行时堆内存GC体系
如何发现
使用 dotMemory,找到boxing是一项基本任务:
- 打开View memory allocations视图。
- 查找值类型的对象(Group by Types),这些都是boxing的结果。
- 确定分配这些对象并生成大部分流量的方法。
当我们尝试将值类型赋值给引用类型时,Heap Allocation Viewer插件也会提示闭包分配的事实:
Boxing allocation: conversion from value type 'int' to reference type 'object'
从性能角度来看,您更感兴趣的是这种闭包发生的频率。例如,如果带有装箱分配的代码只被调用一次,那么优化它不会有太大帮助。考虑到这一点,dotMemory 在检测闭包是否引起真正问题方面要可靠得多。
如何修复
在解决装箱问题之前,请确保它确实会产生大量流量。如果是这样,你的任务就很明确:重写代码以消除装箱。当你引入某些值类型时,请确保不会在代码中的任何位置将值类型转换为引用类型。例如,一个常见的错误是将值类型的变量传递给使用字符串的方法(例如 String.Format
):
int i = 5;
string.Format("i = {0}", i); // 引发box
一个简单的修复方法是调用恰当的值类型 ToString() 方法:
int i = 5;
string.Format("i = {0}", i.ToString());
Resize Collections
动态大小的集合(例如 Dictionary
, List
, HashSet
, 和 StringBuilder
)具有以下特性: 当集合大小超过当前边界时,.NET 会调整集合的大小并在内存中重新定义整个集合。显然,如果这种情况频繁发生,应用程序的性能将会受到影响。
如何发现
使用 dotMemory 比对两个快照
打开View memory allocations视图
找到产生大内存流量的集合类型
看看是否与
Dictionary<>.Resize
、List<>.SetCapacity
、StringBuilder.ExpandByABlock
等等集合扩容有关
如何修复
如果“resize”方法造成的流量很大,唯一的解决方案是减少需要调整大小的情况数量。尝试预测所需的大小并用该大小初始化集合。
var list = new List<string>(1000); // 初始容量1000
此外请记住,任何大于或等于 85,000 字节的分配都会在大对象堆 (LOH) 上进行。在 LOH 中分配内存会带来一些性能损失:由于 LOH 未压缩,因此在分配时需要 CLR 和空闲列表之间进行一些额外的交互。然而,在某些情况下,在 LOH 中分配对象是有意义的,例如,在必须承受应用程序的整个生命周期的大型集合(例如缓存)的情况下。
Enumerating Collections
使用动态集合时,请注意枚举它们的方式。这里典型的主要头痛是使用 foreach
枚举一个集合,只知道它实现了 IEnumerable
接口。考虑以下示例:
class EnumerableTest
{
private void Foo(IEnumerable<string> sList)
{
foreach (var s in sList)
{
}
}
public void Goo()
{
var list = new List<string>();
for (int i = 0; i < 1000; i++)
{
Foo(list);
}
}
}
Foo 方法中的列表被转换为 IEnumerable
接口,这意味着枚举器的进一步装箱,因为List<T>.Enumerator
是结构体。
public struct Enumerator : IEnumerator<T>, IEnumerator, IDisposable
{
public T Current { get; }
object IEnumerator.Current { get; }
public void Dispose();
public bool MoveNext();
void IEnumerator.Reset();
}
如何发现
- 打开View memory allocations视图
- 找到值类型
System.Collections.Generic.List+Enumerator
并检查生成的流量。 - 查找生成这些对象的方法。
- Heap Allocation Viewer插件也会提示您有关隐藏分配的信息:
如何修复
避免将集合强制转换为接口。在上面的示例中,最佳解决方案是创建一个接受 List<string>
集合的 Foo 方法重载。
private void Foo(List<string> sList)
{
foreach (var s in sList)
{
}
}
如果我们在修复后分析代码,会发现 Foo 方法不再创建枚举器。
don’t prematurely optimize
易读性应该在多数时候成为我们编码的第一原则,而非的性能优先或内存优先。本文讨论的一切都是微观优化,定期进行内存分析是良好的习惯
例如,交换a和b,从第一直觉上我们会编写出以下代码:
int a = 5;
int b = 10;
var temp = a;
a = b;
b = temp;
// 在c# 7+我们甚至可以用元组,进一步增强可阅读性
(a, b) = (b, a);
但是下面这种写法通过按位运算,可以不必申请额外空间来存储temp
a = a ^ b;
b = a ^ b;
a = a ^ b;
但这并不是我们鼓励的:过早的在编码初期进行优化,丧失可读性。在99%的情况下,我们的代码应该只依赖语义,剩下的,交给探查器!
上文Boxing提到的string.Format
案例,只能代表今天,而不是明天。也许下一个将在IL编译时甚至JIT中去解决值类型装箱问题,Enumerating Collections也是同一个道理。
int i = 5;
string.Format("i = {0}", i); // 引发box
DefaultInterpolatedStringHandler
.net6引入的ref结构DefaultInterpolatedStringHandler
,就是一个很好的案例
$"..."
这种字符串插值(String Interpolation)语法是在 C# 6.0 中引入的。
var i = 5;
var str = $"i = {i}"; // box
在.net6之前,上面的写法会发生装箱,生成的IL如下:
IL_001a: ldarg.0 // this
IL_001b: ldstr "i = {0}"
IL_0020: ldarg.0 // this
IL_0021: ldfld int32 Fake.EventBus.RabbitMQ.RabbitMqEventBus/'<ProcessingEventAsync>d__19'::'<i>5__1'
IL_0026: box [netstandard]System.Int32
IL_002b: call string [netstandard]System.String::Format(string, object)
IL_0030: stfld string Fake.EventBus.RabbitMQ.RabbitMqEventBus/'<ProcessingEventAsync>d__19'::'<str>5__2'
而从.net6开始,生成的IL发生了变化,由原来调用的System.String::Format(string, object)
,变成了DefaultInterpolatedStringHandler
,装箱也不见了,内部细节感兴趣的自己去阅读源码,内部用到了高性能的Span,unsafe和ArrayPool
IL_0014: ldloca.s V_3
IL_0016: ldc.i4.4
IL_0017: ldc.i4.1
IL_0018: call instance void [System.Runtime]System.Runtime.CompilerServices.DefaultInterpolatedStringHandler::.ctor(int32, int32)
IL_001d: ldloca.s V_3
IL_001f: ldstr "i = "
IL_0024: call instance void [System.Runtime]System.Runtime.CompilerServices.DefaultInterpolatedStringHandler::AppendLiteral(string)
IL_0029: nop
IL_002a: ldloca.s V_3
IL_002c: ldloc.0 // i
IL_002d: call instance void [System.Runtime]System.Runtime.CompilerServices.DefaultInterpolatedStringHandler::AppendFormatted<int32>(!!0/*int32*/)
IL_0032: nop
IL_0033: ldloca.s V_3
IL_0035: call instance string [System.Runtime]System.Runtime.CompilerServices.DefaultInterpolatedStringHandler::ToStringAndClear()
IL_003a: stloc.1 // str
不要过早优化
不要过早优化!!!
不要过早优化!!!
不要过早优化!!!
Link
本系列参考jetbrains官方团队的博客:https://blog.jetbrains.com/dotnet,加以作者的个人理解做出的二次创作,如有侵权请联系删除:2357729423@qq.com。
内存优化:Boxing的更多相关文章
- In-Memory:内存优化表的事务处理
内存优化表(Memory-Optimized Table,简称MOT)使用乐观策略(optimistic approach)实现事务的并发控制,在读取MOT时,使用多行版本化(Multi-Row ve ...
- 试试SQLSERVER2014的内存优化表
试试SQLSERVER2014的内存优化表 SQL Server 2014中的内存引擎(代号为Hekaton)将OLTP提升到了新的高度. 现在,存储引擎已整合进当前的数据库管理系统,而使用先进内存技 ...
- In-Memory:内存优化表 DMV
在内存优化表的DMV中,有两个对象ID(Object ID): xtp_object_id 是内部的内存优化表(Internal Memory-Optimized Table)的ID,在对象的整个生命 ...
- android内存优化
背景 虽然android设备的配置越来越高,但是,由于android系统的机制导致(最主要是app程序的主线程不会真正退出而是在后台常驻内存中) ,这样手机中安装过多的app之后,导致内存被大量占用, ...
- JavaScript内存优化
JavaScript内存优化 相对C/C++ 而言,我们所用的JavaScript 在内存这一方面的处理已经让我们在开发中更注重业务逻辑的编写.但是随着业务的不断复杂化,单页面应用.移动HTML5 应 ...
- [WP8.1UI控件编程]Windows Phone大数据量网络图片列表的异步加载和内存优化
11.2.4 大数据量网络图片列表的异步加载和内存优化 虚拟化技术可以让Windows Phone上的大数据量列表不必担心会一次性加载所有的数据,保证了UI的流程性.对于虚拟化的技术,我们不仅仅只是依 ...
- Unity3D 游戏开发之内存优化
项目的性能优化主要围绕CPU.GPU和内存三大方面进行. 无论是游戏还是VR应用,内存管理都是其研发阶段的重中之重. 然而,在我们测评过的大量项目中,90%以上的项目都存在不同程度的内存使用问题.就目 ...
- java内存优化牛刀小试
小猿做了两年的c++,上个月竟然被调到java项目,于是第一篇随笔就想八一八java的内存优化. 首先优化这种事,肯定是应该放到最后去做的,不过在写代码的过程中养成良好的习惯也是很重要的.在这里先推荐 ...
- .Net内存优化的几点经验
以前从来没有想过.Net开发居然存在内存无法释放的问题,总是认为GC给我处理好了一切.现在GIS二次开发结合三维球开发,没有想到存在如此严重的内存增长,很快内存就不够用了,导致系统各种不稳定.球体和三 ...
- android内存优化相关1
第一种策略,是释放显示相关的内存.这是我们针对系统APP采用的一种调优策略. 图形内容,俗称位图是非常占用内存的,针对位图,我们采用异步加载的方法,将位图内容信息和位图的状态信息分别进行存储,将内容信 ...
随机推荐
- python实现不同颜色气球隔开摆放,并且提示不能摆放的情况
这个是一位隐秘人物让我做的一道题(如标题),我也分享出来了. 首先是成品展示(暂时没有做成可视化界面的样子): 我做的是把所有的气球录入进来,然后利用基础数据结构(字典,数据等)排序等,由于我是初学, ...
- 学Windows批处理第一天:使用批处理命令生成一个文件并写入内容
脚本功能:1.生成一个文件,文件名格式为:yyyymmddhhmmss 2.文件中写入一段文本 操作步骤:1.新建一个文本文档(txt格式) 2.修改文件名为任意名称(我的叫create_file), ...
- 牛客网-SQL专项训练18
①在下列sql语句错误的是?B 解析: 在sql中若要取得NULL,则必须通过IS NULL或者IS NOT NULL进行获取,无法直接使用等号. 一个等号(=)表示把1赋值给变量啊 ==:称为等值符 ...
- 搭建Hadoop环境
搭建Hadoop环境 一.虚拟机的安装 二. 安装JDK 1.下载jdk wget https://download.java.net/openjdk/jdk8u41/ri/openjdk-8u41- ...
- OpenYurt 之 Yurthub 数据过滤框架解析
简介:OpenYurt 是业界首个非侵入的边缘计算云原生开源项目,通过边缘自治,云边协同,边缘单元化,边缘流量闭环等能力为用户提供云边一体化的使用体验.在 Openyurt 里边缘网络可以使用数据过滤 ...
- 技术揭秘:实时数仓Hologres如何支持超大规模部署与运维
简介:在本次评测中,Hologres是目前通过中国信通院大数据产品分布式分析型数据库大规模性能评测的规模最大的MPP数据仓库产品.通过该评测,证明了阿里云实时数仓Hologres能够作为数据仓库和大 ...
- Kettle on MaxCompute使用指南
简介: Kettle是一款开源的ETL工具,纯java实现,可以运行于Windows, Unix, Linux上运行,提供图形化的操作界面,可以通过拖拽控件的方式,方便地定义数据传输的拓扑.Kett ...
- [FE] 被动检测 iframe 加载 src 成功失败的一种思路和方式 (Vue)
思路:设置定时器一个,n 秒后设置 404 或其它,此时给 iframe 的 onload 事件设置回调函数,加载完成则取消定时器. 示例: data () { return { handler: n ...
- 安全机密管理:Asp.Net Core中的本地敏感数据保护技巧
前言 在我们开发过程中基本上不可或缺的用到一些敏感机密数据,比如SQL服务器的连接串或者是OAuth2的Secret等,这些敏感数据在代码中是不太安全的,我们不应该在源代码中存储密码和其他的敏感数据, ...
- 2018-2-13-win10-uwp-无法附加到CoreCLR
title author date CreateTime categories win10 uwp 无法附加到CoreCLR lindexi 2018-2-13 17:23:3 +0800 2018- ...