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