最近在為 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。