最近在为 做性能升级,因此将过程中使用到的 dotTrace 软件的基础用法介绍给各位开发者。
是一个用于轻松应对并发问题的分布式开发框架。如果您是首次阅读本系列文章。建议可以先从本文末尾的入门文章开始了解。
开篇摘要
dotTrace 是 Jetbrains 公司为 .net 应用提供的一款 profile 软件。有助于对于软件中的耗时函数和内存问题进行诊断分析。
本篇,我们将使用 Jetbrains 公司的 dotTrace 软件对一些已知的性能问题进行分析。从而使读者能够掌握使用该软件的基本技能。
过程中我们将搭配一些经典的面试问题进行演示,逐步解释该软件的使用。
此次示例使用的是 Rider 作为主要演示的 IDE。开发者也可以使用 VS + Resharper 做出相同的效果。
如何获取 dotTrace
dotTrace 是付费软件。目前只要购买 dotUltimate 及以上的许可证便可以直接使用该软件。
当然,该软件也包含试用版本,可以免费开启 7 天的试用时间。Jetbrains 的 IDE 购买满一年以上即可获取一个当前最新的永久使用版本。
或者也可以直接购买 Jetbrains 全家桶许可证,一次性全部带走。
经典场景再现
接下来,我们通过一些经典的面试问题,来体验一下如何使用 dotTrace。
何时要使用 StringBuilder
这是多么经典的面试问题。能够看到这篇文章的朋友,我相信各位都知道 StringBuilder 能够减少 string 直接拼接的碎片,减少内存压力这个道理。
我们这是真的吗?会不会只是面试官想要刁难我,欺负我信息不对称呢?
没有关系,接下来,让我们使用 dotTrace 来具体的结合代码来分析一波。看看使用 StringBuilder 究竟有没有减低内存分配的压力。
首先,我们创建一个单元测试项目,并添加以下这样一个测试类:
usingSystem.Linq;
usingSystem.Text;
usingNUnit.Framework;
namespaceNewbe.DotTrace.Tests
{
publicclassX01StringBuilderTest
{
[ Test]
publicvoidUsingString
{
varsource = ( 0, 10)
.Select(x => )
.ToArray;
varre = string.Empty;
for( inti = 0; i < 10_000; i++)
{
re += source[i % 10];
}
}
[ Test]
publicvoidUsingStringBuilder
{
varsource = ( 0, 10)
.Select(x => )
.ToArray;
varsb = newStringBuilder;
for( vari = 0; i < 10_000; i++)
{
(source[i % 10]);
}
var_ =
}
}
}
然后,如下图所示,我们将 Rider 中的 profile 模式设置为 Timeline 。
TimeLine 是多种模式中的一种,相较而言,该模式可以更全面的了解各个线程的工作情况,包括有内存分配、IO 处理、锁、反射等等多维度数据。这将会作为本示例主要使用的一种模式。
接着,如下图所示,通过单元测试左侧的小图标启动对应测试的 profile。
启动 profile 之后,等待一段时间之后,便会出现最新生成的 timeline 报告。查看报告的位置如下所示:
右键选择对应的报告,选择”Open in External Viewer”,便可以使用 dotTrace 打开生成好的报告。
那么首先,让我打开第一个报告,查看 UsingString 方法生成的报告。
如下图所示,选择 .Net Memory Allocations 以查看该测试运行过程中分配的内存数额。
根据上图我们可以得出以下结论:
- 在这测试中,有 102M 的内存被分配给 String 。注意,在 dotTrace 中显示的分配是指整个运行过程中全部分配的内存。即使后续被回收,该数值也不会减少。
- 内存的分配只要在 CLR Worker 线程进行。并且非常的密集。
Tip:Timeline 所显示的运行时间比正常运行测试的时间更长,因为在 profile 过程中需要对数据进行记录会有额外的消耗。
因此,我们就得出了第一个结论:使用 string 进行直接拼接,确实会消耗更多的内存分配。
接着,我们继续按照上面的步骤,查看一下 UsingStringBuilder 方法的报告,如下所示:
根据上图,我们可以得出第二个结论:使用 StringBuilder 可以明显的减少相较于 string 直接拼接所消耗的内存。
当然,我们得到的最终的结论其实是:看来面试官不是糊弄人。
class 和 struct 对内存有什么影响
class 和 struct 的区别有很多,面试题常客了。其中,两者在内存方面就存在区别。
那么我们通过一个测试来看看区别。
usingSystem;
usingSystem.Collections.Generic;
usingNUnit.Framework;
namespaceNewbe.DotTrace.Tests
{
publicclassX02ClassAndStruct
{
[ Test]
publicvoidUsingClass
{
( $"memory in bytes before execution: {}");
constintcount = 1_000_000;
varlist = newList(count);
for( vari = 0; i < count; i++)
{
( newStudent
{
Level = int.MinValue
});
}
list.Clear;
vargcMemoryInfo = GC.GetGCMemoryInfo;
( $"heap size: {}");
( $"memory in bytes end of execution: {}");
}
[ Test]
publicvoidUsingStruct
{
( $"memory in bytes before execution: {}");
constintcount = 1_000_000;
varlist = newList(count);
for( vari = 0; i < count; i++)
{
( newYueluo
{
Level = int.MinValue
});
}
list.Clear;
vargcMemoryInfo = GC.GetGCMemoryInfo;
( $"heap size: {}");
( $"memory in bytes end of execution: {}");
}
publicclassStudent
{
publicintLevel { get; set; }
}
publicstructYueluo
{
publicintLevel { get; set; }
}
}
}
代码要点:
- 两个测试,分别创建 1,000,000 个 class 和 struct 加入到 List 中。
- 运行测试之后,在测试的末尾输出当前堆空间的大小。
按照上一节提供的基础步骤,我们对比两个方法生成的报告。
UsingClass
UsingStruct
对比两个报告,可以得出以下这些结论:
- Timeline 报告中的内存分配,只包含分配在堆上的内存情况。
- struct 不需要分配在堆上,但是,数组是引用对象,需要分配在堆上。
- List 自增的过程本质是扩张数组的特性在报告中也得到了体现。
- 另外,没有展示在报告上,而展示在测试打印文本中可以看到,UsingStruct 运行之后的堆大小也证实了 struct 不会被分配在堆上。
装箱和拆箱
经典面试题 X3,来,上代码,上报告!
usingNUnit.Framework;
namespaceNewbe.DotTrace.Tests
{
publicclassX03Boxing
{
[ Test]
publicvoidBoxing
{
for( inti = 0; i < 1_000_000; i++)
{
UseObject(i);
}
}
[ Test]
publicvoidNoBoxing
{
for( inti = 0; i < 1_000_000; i++)
{
UseInt(i);
}
}
publicstaticvoidUseInt( intage )
{
// nothing
}
publicstaticvoidUseObject( objectobj )
{
// nothing
}
}
}
Boxing, 发生装箱拆箱
NoBoxing,未发生装箱拆箱
对比两个报告,可以得出以下这些结论:
- 没有买卖就没有杀害,没有装拆就没有分配消耗。
和 有什么区别
经典面试题 X4,来,上代码,上报告!
usingSystem;
usingSystem.Collections.Generic;
usingSystem.Threading;
usingSystem.Threading.Tasks;
usingNUnit.Framework;
namespaceNewbe.DotTrace.Tests
{
publicclassX04SleepTest
{
[ Test]
publicTask TaskDelay
{
return(( 3));
}
[ Test]
publicTask ThreadSleep
{
returnTask.Run( => { (( 3)); });
}
}
}
ThreadSleep
TaskDelay
对比两个报告,可以得出以下这些结论:
- 在 dotTrace 中 会被单独标记,因为这是一种性能不不佳的做法,容易造成线程饥饿。
- 比起 会多出一个线程处于 Sleep 状态
阻塞大量的 Task 真的会导致应用一动不动吗
有了上一步的结论,笔者产生了一个大胆的想法。我们都知道线程的有限的,那如果启动非常多的 或者 会如何呢?
来,代码:
usingSystem;
usingSystem.Collections.Generic;
usingSystem.Threading;
usingSystem.Threading.Tasks;
usingNUnit.Framework;
namespaceNewbe.DotTrace.Tests
{
publicclassX04SleepTest
{
[ Test]
publicTask RunThreadSleep
{
returnTask.WhenAny(GetTasks( 50));
IEnumerableGetTasks( intcount )
{
for( inti = 0; i < count; i++)
{
vari1 = i;
yieldreturnTask.Run( =>
{
( $"Task {i1}");
( int.MaxValue);
});
}
yieldreturnTask.Run( => { ( "yueluo is the only one dalao"); });
}
}
[ Test]
publicTask RunTaskDelay
{
returnTask.WhenAny(GetTasks( 50));
IEnumerableGetTasks( intcount )
{
for( inti = 0; i < count; i++)
{
vari1 = i;
yieldreturnTask.Run( =>
{
( $"Task {i1}");
return(( int.MaxValue));
});
}
yieldreturnTask.Run( => { ( "yueluo is the only one dalao"); });
}
}
}
}
这里就不贴报告了,读者可以试一下这个测试,也可以将报告的内容写在本文的评论中参与讨论~
反射调用和表达式树编译调用
有时,我们需要动态调用一个方法。最广为人知的方式就是使用反射。
但是,这也是广为人知的耗时相对较高的方式。
这里,笔者提供一种使用表达式树创建委托来取代反射提高效率的思路。
那么,究竟有没有减少时间消耗呢?好报告,自己会说话。
usingSystem;
usingSystem.Diagnostics;
usingSystem.Linq.Expressions;
usingNUnit.Framework;
namespaceNewbe.DotTrace.Tests
{
publicclassX05ReflectionTest
{
[ Test]
publicvoidRunReflection
{
varmethodInfo = ( nameof(MoYue));
(methodInfo != null, nameof(methodInfo) + " != null");
for( inti = 0; i < 1_000_000; i++)
{
( null, null);
}
(_count);
}
[ Test]
publicvoidRunExpression
{
varmethodInfo = ( nameof(MoYue));
(methodInfo != null, nameof(methodInfo) + " != null");
varmethodCallExpression = (methodInfo);
varlambdaExpression = (methodCallExpression);
varfunc = lambdaExpression.Compile;
for( inti = 0; i < 1_000_000; i++)
{
}
(_count);
}
privatestaticint_count = 0;
publicstaticvoidMoYue
{
_count++;
}
}
}
RunReflection,直接使用反射调用。
RunExpression,使用表达式树编译一个委托。
本篇小结
使用 dotTrace 可以查看方法的内存和时间消耗。本篇所演示的内容只是其中很小的部分。开发者们可以尝试上手,大有裨益。
本篇内容中的示例代码,均可以在以下链接仓库中找到:
- https://github.com/newbe36524/Newbe.Demo
- https://gitee.com/yks/Newbe.Demo
最后但是最重要!
如果读者对该内容感兴趣,欢迎转发、评论、收藏文章以及项目。
最近作者正在构建以 反应式 、 Actor模式 和 事件溯源 为理论基础的一套服务端开发框架。希望为开发者提供能够便于开发出 “分布式”、“可水平扩展”、“可测试性高” 的应用系统 ——
本篇文章是该框架的一篇技术选文,属于技术构成的一部分。
联系方式:
- Github Issue
- Gitee Issue
- 公开邮箱 newbe-claptrap@ (发送到该邮箱的内容将被公开)
- Gitter
您还可以查阅本系列的其他选文:
理论入门篇
- - 一套以 “事件溯源” 和 “Actor 模式” 作为基本理论的服务端开发框架
术语介绍篇
- Actor 模式
- 事件溯源(Event Sourcing)
- Claptrap
- Minion
- 事件 (Event)
- 状态 (State)
- 状态快照 (State Snapshot)
- Claptrap 设计图 (Claptrap Design)
- Claptrap 工厂 (Claptrap Factory)
- Claptrap Identity
- Claptrap Box
- Claptrap 生命周期(Claptrap Lifetime Scope)
- 序列化(Serialization)
实现入门篇
- 框架入门,第一步 —— 创建项目,实现简易购物车
- 框架入门,第二步 —— 简单业务,清空购物车
- 框架入门,第三步 —— 定义 Claptrap,管理商品库存
- 框架入门,第四步 —— 利用 Minion,商品下单
样例实践篇
- 构建一个简易的火车票售票系统, 框架用例,第一步 —— 业务分析
- 在线体验火车票售票系统
其他番外篇
- 谈反应式编程在服务端中的应用,数据库操作优化,从 20 秒到 秒
- 谈反应式编程在服务端中的应用,数据库操作优化,提速 Upsert
- 十万同时在线用户,需要多少内存?—— 框架水平扩展实验
- docker-mcr 助您全速下载 dotnet 镜像
- 十多位全球技术专家,为你献上近十个小时的.Net 微服务介绍
- 年轻的樵夫哟,你掉的是这个免费 8 核 4G 公网服务器,还是这个随时可用的 Docker 实验平台?
- 如何使用 dotTrace 来诊断 netcore 应用的性能问题
GitHub 项目地址:https://github.com/newbe36524/
Gitee 项目地址:https://gitee.com/yks/
您当前查看的是先行发布于 上的博客文章,实际开发文档随版本而迭代。若要查看最新的开发文档,需要移步 。