您的位置:首頁(yè) > 軟件教程 > 教程 > 內(nèi)存優(yōu)化:Boxing

內(nèi)存優(yōu)化:Boxing

來(lái)源:好特整理 | 時(shí)間:2024-06-06 18:49:15 | 閱讀:117 |  標(biāo)簽: XING in   | 分享到:

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),如何減少呢? “普通”性

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),如何減少呢? “普通”性能分析不會(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)短:

  • dotMemory 內(nèi)存分析器。無(wú)論您試圖查找什么問(wèn)題,分析算法始終相同:
    • 在啟用內(nèi)存流量收集的情況下開始分析您的應(yīng)用程序。
    • 在您感興趣的方法或功能完成工作后收集內(nèi)存快照。
    • 打開快照并選擇內(nèi)存流量視圖。
  • Heap Allocations Viewer插件。該插件會(huì)突出顯示代碼中分配內(nèi)存的所有位置。這不是必須的,但它使編碼更加方便,并且在某種意義上“迫使”您避免過(guò)度分配。

Boxing

裝箱是將值類型轉(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體系

如何發(fā)現(xiàn)

使用 dotMemory,找到boxing是一項(xiàng)基本任務(wù):

  1. 打開View memory allocations視圖。
  2. 查找值類型的對(duì)象(Group by Types),這些都是boxing的結(jié)果。
  3. 確定分配這些對(duì)象并生成大部分流量的方法。

當(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)題方面要可靠得多。

如何修復(fù)

在解決裝箱問(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());

Resize Collections

動(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ì)受到影響。

如何發(fā)現(xiàn)

使用 dotMemory 比對(duì)兩個(gè)快照

  1. 打開View memory allocations視圖

  2. 找到產(chǎn)生大內(nèi)存流量的集合類型

  3. 看看是否與 Dictionary<>.Resize List<>.SetCapacity 、 StringBuilder.ExpandByABlock 等等集合擴(kuò)容有關(guān)

內(nèi)存優(yōu)化:Boxing

如何修復(fù)

如果“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è)生命周期的大型集合(例如緩存)的情況下。

Enumerating Collections

使用動(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.Enumerator 是結(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();
}

如何發(fā)現(xiàn)

  1. 打開View memory allocations視圖
  2. 找到值類型 System.Collections.Generic.List+Enumerator 并檢查生成的流量。
  3. 查找生成這些對(duì)象的方法。
  4. Heap Allocation Viewer插件也會(huì)提示您有關(guān)隱藏分配的信息:

內(nèi)存優(yōu)化:Boxing

如何修復(fù)

避免將集合強(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)建枚舉器。

don’t prematurely optimize

易讀性應(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

DefaultInterpolatedStringHandler

.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)化。!

不要過(guò)早優(yōu)化!!

Link

本系列參考jetbrains官方團(tuán)隊(duì)的博客: https://blog.jetbrains.com/dotnet,加以作者的個(gè)人理解做出的二次創(chuàng)作,如有侵權(quán)請(qǐng)聯(lián)系刪除:[email protected]

小編推薦閱讀

好特網(wǎng)發(fā)布此文僅為傳遞信息,不代表好特網(wǎng)認(rèn)同期限觀點(diǎn)或證實(shí)其描述。

相關(guān)視頻攻略

更多

掃二維碼進(jìn)入好特網(wǎng)手機(jī)版本!

掃二維碼進(jìn)入好特網(wǎng)微信公眾號(hào)!

本站所有軟件,都由網(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)