最近在為 Newbe.Claptrap 做性能升級,因此將過程中使用到的 dotTrace 軟件的基礎用法介紹給各位開發者。
Newbe.Claptrap 是一個用於輕松應對並發問題的分布式開發框架。如果您是首次閱讀本系列文章。建議可以先從本文末尾的入門文章開始了解。
開篇摘要
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 線程進行。並且非常的密集。
Tip: Timeline 所顯示的運行時間比正常運行測試的時間更長,因為在 profile 過程中需要對數據進行記錄會有額外的消耗。
因此,我們就得出了第一個結論:使用 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
- 公開郵箱 newbe-claptrap@googlegroups.com (發送到該郵箱的內容將被公開)
- 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
您當前查看的是先行發布於 www.newbe.pro 上的博客文章,實際開發文檔隨版本而迭代。若要查看最新的開發文檔,需要移步 claptrap.newbe.pro。