dotMemory 如今,許多開發(fā)人員都熟悉性能分析的工作流程:在分析器下運(yùn)行應(yīng)用程序,測(cè)量方法的執(zhí)行時(shí)間,識(shí)別占用時(shí)間較多的方法,并致力于優(yōu)化它們。然而,這種情況并沒(méi)有涵蓋到一個(gè)重要的性能指標(biāo):應(yīng)用程序多次GC所分配的時(shí)間。當(dāng)然,你可以評(píng)估GC所需的總時(shí)間,但是它從哪里來(lái),如何減少呢? “普通”性
如今,許多開發(fā)人員都熟悉性能分析的工作流程:在分析器下運(yùn)行應(yīng)用程序,測(cè)量方法的執(zhí)行時(shí)間,識(shí)別占用時(shí)間較多的方法,并致力于優(yōu)化它們。然而,這種情況并沒(méi)有涵蓋到一個(gè)重要的性能指標(biāo):應(yīng)用程序多次GC所分配的時(shí)間。當(dāng)然,你可以評(píng)估GC所需的總時(shí)間,但是它從哪里來(lái),如何減少呢? “普通”性能分析不會(huì)給你任何線索。
垃圾收集總是由高內(nèi)存流量引起的:分配的內(nèi)存越多,需要收集的內(nèi)存就越多。眾所周知,內(nèi)存流量?jī)?yōu)化應(yīng)該在內(nèi)存分析器的幫助下完成。它允許你確定對(duì)象是如何分配和收集的,以及這些分配背后保留了哪些方法。理論上看起來(lái)很簡(jiǎn)單,對(duì)吧?然而,在實(shí)踐中,許多開發(fā)人員最終都會(huì)這樣說(shuō):“好吧,我的應(yīng)用程序中的一些流量是由一些系統(tǒng)類生成的,這些系統(tǒng)類的名稱是我一生中第一次看到的。我想這可能是因?yàn)橐恍┰愀獾拇a設(shè)計(jì),F(xiàn)在我該怎么做?”
這就是這篇文章的主題。實(shí)際上,這將是一系列文章,我將在其中分享我的內(nèi)存流量分析經(jīng)驗(yàn):我認(rèn)為什么是“糟糕的代碼設(shè)計(jì)”,如何在內(nèi)存中找到其蹤跡,當(dāng)然還有我認(rèn)為的最佳實(shí)踐。
簡(jiǎn)單的例子:如果您在堆中看到值類型的對(duì)象,那么裝箱肯定是罪魁禍?zhǔn)。裝箱總是意味著額外的內(nèi)存分配,因此移除它很可能會(huì)讓您的應(yīng)用程序變得更好。
該系列的第一篇文章將重點(diǎn)關(guān)注裝箱。如果檢測(cè)到“bad memory pattern”,該去哪里查找以及如何采取行動(dòng)?
本系列中描述的最佳實(shí)踐使我們能夠?qū)?.NET 產(chǎn)品中某些算法的性能提高 20%-50%。
在我們進(jìn)一步討論之前,先看看我們需要的工具。我們?cè)?JetBrains 使用的工具列表非常簡(jiǎn)短:
裝箱是將值類型轉(zhuǎn)換為引用類型。 例如:
int i = 5;
object o = i; // 發(fā)生裝箱
為什么這是個(gè)問(wèn)題?值類型存儲(chǔ)在棧中,而引用類型存儲(chǔ)在托管堆中。因此,要將整數(shù)值分配給對(duì)象,CLR 必須從棧中取出該值并將其復(fù)制到堆中。當(dāng)然,這種移動(dòng)會(huì)影響應(yīng)用程序的性能。
一個(gè)對(duì)象的至少占用3個(gè)指針單元:對(duì)象頭(object header)、方法表指針(method table ref)、預(yù)留單元(首字段地址/數(shù)組長(zhǎng)度)
在x64系統(tǒng)3個(gè)指針單元意味24字節(jié)的開銷,而一個(gè)int類型本身只占用4字節(jié),其次,棧內(nèi)存的由執(zhí)行線程方法棧管理,方法內(nèi)聲明的local變量、字面量更是能夠在IL編譯期就預(yù)算出棧容量,效率遠(yuǎn)高于運(yùn)行時(shí)堆內(nèi)存GC體系
使用 dotMemory,找到boxing是一項(xiàng)基本任務(wù):
當(dāng)我們嘗試將值類型賦值給引用類型時(shí),Heap Allocation Viewer插件也會(huì)提示閉包分配的事實(shí):
? Boxing allocation: conversion from value type 'int' to reference type 'object'
從性能角度來(lái)看,您更感興趣的是這種閉包發(fā)生的頻率。例如,如果帶有裝箱分配的代碼只被調(diào)用一次,那么優(yōu)化它不會(huì)有太大幫助?紤]到這一點(diǎn),dotMemory 在檢測(cè)閉包是否引起真正問(wèn)題方面要可靠得多。
在解決裝箱問(wèn)題之前,請(qǐng)確保它確實(shí)會(huì)產(chǎn)生大量流量。如果是這樣,你的任務(wù)就很明確:重寫代碼以消除裝箱。當(dāng)你引入某些值類型時(shí),請(qǐng)確保不會(huì)在代碼中的任何位置將值類型轉(zhuǎn)換為引用類型。例如,一個(gè)常見的錯(cuò)誤是將值類型的變量傳遞給使用字符串的方法(例如
String.Format
):
int i = 5;
string.Format("i = {0}", i); // 引發(fā)box
一個(gè)簡(jiǎn)單的修復(fù)方法是調(diào)用恰當(dāng)?shù)闹殿愋?ToString() 方法:
int i = 5;
string.Format("i = {0}", i.ToString());
動(dòng)態(tài)大小的集合(例如
Dictionary
,
List
,
HashSet
, 和
StringBuilder
)具有以下特性: 當(dāng)集合大小超過(guò)當(dāng)前邊界時(shí),.NET 會(huì)調(diào)整集合的大小并在內(nèi)存中重新定義整個(gè)集合。顯然,如果這種情況頻繁發(fā)生,應(yīng)用程序的性能將會(huì)受到影響。
使用 dotMemory 比對(duì)兩個(gè)快照
打開View memory allocations視圖
找到產(chǎn)生大內(nèi)存流量的集合類型
看看是否與
Dictionary<>.Resize
、
List<>.SetCapacity
、
StringBuilder.ExpandByABlock
等等集合擴(kuò)容有關(guān)
如果“resize”方法造成的流量很大,唯一的解決方案是減少需要調(diào)整大小的情況數(shù)量。嘗試預(yù)測(cè)所需的大小并用該大小初始化集合。
var list = new List(1000); // 初始容量1000
此外請(qǐng)記住,任何大于或等于 85,000 字節(jié)的分配都會(huì)在大對(duì)象堆 (LOH) 上進(jìn)行。在 LOH 中分配內(nèi)存會(huì)帶來(lái)一些性能損失:由于 LOH 未壓縮,因此在分配時(shí)需要 CLR 和空閑列表之間進(jìn)行一些額外的交互。然而,在某些情況下,在 LOH 中分配對(duì)象是有意義的,例如,在必須承受應(yīng)用程序的整個(gè)生命周期的大型集合(例如緩存)的情況下。
使用動(dòng)態(tài)集合時(shí),請(qǐng)注意枚舉它們的方式。這里典型的主要頭痛是使用
foreach
枚舉一個(gè)集合,只知道它實(shí)現(xiàn)了
IEnumerable
接口?紤]以下示例:
class EnumerableTest
{
private void Foo(IEnumerable sList)
{
foreach (var s in sList)
{
}
}
public void Goo()
{
var list = new List();
for (int i = 0; i < 1000; i++)
{
Foo(list);
}
}
}
Foo 方法中的列表被轉(zhuǎn)換為
IEnumerable
接口,這意味著枚舉器的進(jìn)一步裝箱,因?yàn)?
List
是結(jié)構(gòu)體。
public struct Enumerator : IEnumerator, IEnumerator, IDisposable
{
public T Current { get; }
object IEnumerator.Current { get; }
public void Dispose();
public bool MoveNext();
void IEnumerator.Reset();
}
System.Collections.Generic.List+Enumerator
并檢查生成的流量。
避免將集合強(qiáng)制轉(zhuǎn)換為接口。在上面的示例中,最佳解決方案是創(chuàng)建一個(gè)接受
List
集合的 Foo 方法重載。
private void Foo(List sList)
{
foreach (var s in sList)
{
}
}
如果我們?cè)谛迯?fù)后分析代碼,會(huì)發(fā)現(xiàn) Foo 方法不再創(chuàng)建枚舉器。
易讀性應(yīng)該在多數(shù)時(shí)候成為我們編碼的第一原則,而非的性能優(yōu)先或內(nèi)存優(yōu)先。本文討論的一切都是微觀優(yōu)化,定期進(jìn)行內(nèi)存分析是良好的習(xí)慣
例如,交換a和b,從第一直覺(jué)上我們會(huì)編寫出以下代碼:
int a = 5;
int b = 10;
var temp = a;
a = b;
b = temp;
// 在c# 7+我們甚至可以用元組,進(jìn)一步增強(qiáng)可閱讀性
(a, b) = (b, a);
但是下面這種寫法通過(guò)按位運(yùn)算,可以不必申請(qǐng)額外空間來(lái)存儲(chǔ)temp
a = a ^ b;
b = a ^ b;
a = a ^ b;
但這并不是我們鼓勵(lì)的:過(guò)早的在編碼初期進(jìn)行優(yōu)化,喪失可讀性。在99%的情況下,我們的代碼應(yīng)該只依賴語(yǔ)義,剩下的,交給探查器!
上文Boxing提到的
string.Format
案例,只能代表今天,而不是明天。也許下一個(gè)將在IL編譯時(shí)甚至JIT中去解決值類型裝箱問(wèn)題,Enumerating Collections也是同一個(gè)道理。
int i = 5;
string.Format("i = {0}", i); // 引發(fā)box
.net6引入的ref結(jié)構(gòu)
DefaultInterpolatedStringHandler
,就是一個(gè)很好的案例
$"..."
這種字符串插值(String Interpolation)語(yǔ)法是在 C# 6.0 中引入的。
var i = 5;
var str = $"i = {i}"; // box
在.net6之前,上面的寫法會(huì)發(fā)生裝箱,生成的IL如下:
IL_001a: ldarg.0 // this
IL_001b: ldstr "i = {0}"
IL_0020: ldarg.0 // this
IL_0021: ldfld int32 Fake.EventBus.RabbitMQ.RabbitMqEventBus/'d__19'::'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/'d__19'::'5__2'
而從.net6開始,生成的IL發(fā)生了變化,由原來(lái)調(diào)用的
System.String::Format(string, object)
,變成了
DefaultInterpolatedStringHandler
,裝箱也不見了,內(nèi)部細(xì)節(jié)感興趣的自己去閱讀源碼,內(nèi)部用到了高性能的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(!!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
不要過(guò)早優(yōu)化!!
不要過(guò)早優(yōu)化。!
不要過(guò)早優(yōu)化!!
本系列參考jetbrains官方團(tuán)隊(duì)的博客: https://blog.jetbrains.com/dotnet,加以作者的個(gè)人理解做出的二次創(chuàng)作,如有侵權(quán)請(qǐng)聯(lián)系刪除:[email protected]。
機(jī)器學(xué)習(xí):神經(jīng)網(wǎng)絡(luò)構(gòu)建(下)
閱讀華為Mate品牌盛典:HarmonyOS NEXT加持下游戲性能得到充分釋放
閱讀實(shí)現(xiàn)對(duì)象集合與DataTable的相互轉(zhuǎn)換
閱讀鴻蒙NEXT元服務(wù):論如何免費(fèi)快速上架作品
閱讀算法與數(shù)據(jù)結(jié)構(gòu) 1 - 模擬
閱讀5. Spring Cloud OpenFeign 聲明式 WebService 客戶端的超詳細(xì)使用
閱讀Java代理模式:靜態(tài)代理和動(dòng)態(tài)代理的對(duì)比分析
閱讀Win11筆記本“自動(dòng)管理應(yīng)用的顏色”顯示規(guī)則
閱讀本站所有軟件,都由網(wǎng)友上傳,如有侵犯你的版權(quán),請(qǐng)發(fā)郵件[email protected]
湘ICP備2022002427號(hào)-10 湘公網(wǎng)安備:43070202000427號(hào)© 2013~2025 haote.com 好特網(wǎng)