LOGO OA教程 ERP教程 模切知識交流 PMS教程 CRM教程 開發文檔 其他文檔  
 
網站管理員

如何設計內存分配策略?.NET Core 內存分配策略 對象分配(Alloc)底層原理淺談

freeflydom
2025年1月15日 9:15 本文熱度 392

.NET程序的對象是由CLR控制并分配在托管堆中,如果是你,會如何設計一個內存分配策略呢?

  1. 按需分配,要多少分配多少,移動alloc_ptr指針即可,沒有任何浪費。缺點是每次都要向OS申請內存,效率低

  2. 預留緩沖區,降低了向OS申請內存的頻次。但在多線程情況下,alloc_ptr鎖競爭會非常激烈,同樣會降低效率

  3. 利用TLS,來避免鎖競爭,但Thread1與Thread2之間存在Free塊,內存空間浪費多。

  4. 將Free塊利用起來,實現最大化能效

.NET程序就是采用了第四種,來實現空間與時間的最大化。

因此有些alloc_context在段尾,有些在兩個已分配對象之間的空余空間

眼見為實

Free塊

在上面,我們已經見到了Free塊,簡單來說,Free就是segment中留下來的空洞。也就是內存碎片。

Free塊產生的原因主要有三種

  1. GC計劃階段,用作特殊目的
  2. GC標記階段,將垃圾對象標記為Free
  3. 多個Free相鄰時,GC將多個合并為一個大的Free塊

內存碎片的危害:造成內存空間的浪費,降低內存分配效率。

眼見為實

點擊查看代碼
    internal class Program
    {
        public static byte[] bytes1, bytes2, bytes3, bytes4, bytes5, bytes6;
        static void Main(string[] args)
        {
            Alloc();
            Console.WriteLine("分配完畢");
            Debugger.Break();
            GC.Collect();
            Console.WriteLine("GC完成");
            Debugger.Break();
        }
        public static void Alloc()
        {
            bytes1 = new byte[85000];
            bytes2 = new byte[85000];
            bytes3 = new byte[85000];
            bytes4 = new byte[85000];
            bytes5 = new byte[85000];
            bytes6 = new byte[85000];
            bytes2 = null;
            bytes3 = null;//將2,3標記為Free,兩個相鄰Free會合并
            bytes5 = null;
        }
    }


LOH特有的特殊標記


垃圾對象被標記為Free,相鄰的Free對象合并。

Free塊管理邏輯

CLR對Free塊采用數組+鏈表進行管理,根據size確定對應的bucket,再使用雙向鏈表維系大小相近的Free塊。

這樣按照大小維度劃分,極大提高了查找的性能,拿空間換時間。

眼見為實

不是所有Free都會被納入管理,只有 free > 2 * min_obj_size 的對象才能進入bucket集合中,可以思考一下為什么會這么做。

Free塊內存結構

與其它普通對象類似,也有對象頭與方法表,再附加額外信息。內存結構如下。

眼見為實

點擊查看代碼
    internal class Program
    {
        public static byte[] bytes1, bytes2, bytes3, bytes4, bytes5, bytes6;
        static void Main(string[] args)
        {
            Alloc();
            Console.WriteLine("分配完畢");
            Debugger.Break();
            GC.Collect();
            Console.WriteLine("GC完成");
            Debugger.Break();
        }
        public static void Alloc()
        {
            bytes1 = new byte[85000];
            bytes2 = new byte[85000];
            bytes3 = new byte[85000];
            bytes4 = new byte[85000];
            bytes5 = new byte[85000];
            bytes6 = new byte[85000];
            bytes2 = null;
            bytes4 = null;
            bytes6 = null;
        }
    }

細心的朋友會發現兩個問題

  1. do命令顯示的size明明為0x14c60,為什么dp命令顯示為0x14c48?
    只是計算取值的差異,它們之間差值為24,分別為objectheader/mt/size
  2. 為什么Next Free有值,Prev Free沒有值?
    在SOH上2代Free記錄了Next/Prev Free的雙向鏈表。 其他代只記錄了Next Free的單向鏈表

對象分配過程

點擊查看代碼
    internal class Program
    { 
        static void Main(string[] args)
        {
            Debugger.Break();
            var person = new Person();
            Debugger.Break();
        }
    }
    public class Person
    {
        private long age = 10;
        private int age2 = 10;
        private int age3 = 10;
    }

new Person() 的匯編如下

00007ffb`93b8195e 48b9d893c393fb7f0000 mov rcx,7FFB93C393D8h (MT: Example_12_1_4.Person)
00007ffb`93b81968 e8d3ceb65f      call    coreclr!JIT_TrialAllocSFastMP_InlineGetThread (00007ffb`f36ee840)
00007ffb`93b8196d 488945f0        mov     qword ptr [rbp-10h],rax
00007ffb`93b81971 488b4df0        mov     rcx,qword ptr [rbp-10h]
00007ffb`93b81975 ff15b5df0a00    call    qword ptr [00007ffb`93c2f930 (Example_12_1_4.Person..ctor())
00007ffb`93b8197b 488b45f0        mov     rax,qword ptr [rbp-10h]
00007ffb`93b8197f 488945f8        mov     qword ptr [rbp-8],rax

可以看到,New對象的創建分為兩步:

  1. 先調用JIT_TrialAllocSFastMP_InlineGetThread進行內存分配
; IN: rcx: MethodTable*
; OUT: rax: new object
LEAF_ENTRY JIT_TrialAllocSFastMP_InlineGetThread, _TEXT
        mov     edx, [rcx + OFFSET__MethodTable__m_BaseSize]
        ; m_BaseSize is guaranteed to be a multiple of 8.  以8byte為步進,結合MT得出需要的內存空間
        INLINE_GETTHREAD r11
        mov     r10, [r11 + OFFSET__Thread__m_alloc_context__alloc_limit]
        mov     rax, [r11 + OFFSET__Thread__m_alloc_context__alloc_ptr]
        add     rdx, rax ;內存分配后ptr指針的位置
        cmp     rdx, r10 ;//判斷ptr>limit,說明內存不足,執行AllocFailed邏輯。
        ja      AllocFailed
        mov     [r11 + OFFSET__Thread__m_alloc_context__alloc_ptr], rdx ;反之更新ptr指針位置,完成內存分配
        mov     [rax], rcx
        ret

    AllocFailed:
        jmp     JIT_NEW ;走慢速路徑,盡最大可能保證分配成功
LEAF_END JIT_TrialAllocSFastMP_InlineGetThread, _TEXT
  1. 再調用構造函數進行值分配

在Example_12_1_4.Person..ctor()處設置斷點


快速路徑與慢速路徑

在上面提到過的JIT_TrialAllocSFastMP_InlineGetThread方法中,可以看到當Alloc_limit不足,不能完成內存分配時,會執行JIT_NEW方法。
JIT_NEW內部有大量判斷,來盡最大可能保證分配成功。因此執行速度比較慢,所以稱為慢速路徑,與之對應的JIT_TrialAllocSFastMP_InlineGetThread方法,判斷極其簡單且高效,所以被稱之為快速路徑

JIT在編譯期間會根據不同的對象來使用不同的策略,比如帶析構函數的類默認是慢速分配。

慢速分配流程圖如下:

可以看到,快速分配僅僅用8行匯編就完成了分配過程,而慢速分配則復雜得多。

眼見為實

點擊查看代碼
    internal class Program
    {
        static void Main(string[] args)
        {
            byte[] b = new byte[86000];//.NET Core 2.1中,大對象會走慢速分配,.NET8中已經優化了
            var p = new Person();//不帶析構函數,走快速分配
            var pf = new PersonFinalize();//帶析構函數,走慢速分配
            Debugger.Break();
        }
    }
    public class Person
    {
    }
    public class PersonFinalize
    {
        ~PersonFinalize()
        {
        }
    }

避免堆分配

到目前位置,我們討論都是在托管堆上的分配,也了解到.NET如何盡可能使堆分配更高效。
眾所周知,在棧上進行分配與釋放的速度明顯要快得多,完全避免了堆過程。因此在特定條件下,棧分配是一個非常有空且高效的操作

如果我們希望非常高效地處理數據同時又不想再堆上分配大型數據表,可以顯示使用棧分配方式
棧分配方式主要有兩種:

  1. stackalloc
        unsafe void Test()
        {
            int* array = stackalloc int[20];//new int[20] 只是讓數據排列更緊密,本質還是分配在堆上。
            array[0] = 10;
            array[19] = 12;
			Debugger.Break();
        }

  1. Span
        public void SpanTest()
        {
            Span<int> array = stackalloc int[20];
            array[0] = 10;
            array[19] = 12;
            Debugger.Break();
        }

顯式使用棧分配能帶來兩個好處

  1. 分配在棧上,永遠不會進入慢速分支,也完全不受GC管轄,但要注意StaticOverflow
  2. 由于生命周期跟隨棧指針,對象的地址也被變相的固定住(不會移動),所以可以安全的將內存地址傳遞給非托管代碼,且不會產生額外的固定(pinning)

固定(pinning)對象是影響GC速度的大殺手

總結

在性能要求非常高的情況下,盡量避免堆分配。如果條件允許,在棧上分配是更好的選擇,如果條件不允許(StaticOverflow),使用對象池機制實現對象復用也是一種好的解決辦法

基于這個思路,我們會發現日常編碼中,有很多值得優化的地方

  1. 使用結構傳遞小型數據
  2. 使用ValueTuple替代Tuple
  3. 使用stackalloc分配小型數組,或者使用ArrayPool實現對象復用
  4. 針對經常被緩存的Task,使用ValueTask
  5. 閉包帶來的值類型提升
  6. 使用ValueTuple替代匿名對象
  7. ........................

轉自https://www.cnblogs.com/lmy5215006/p/18647110


該文章在 2025/1/15 9:15:01 編輯過
關鍵字查詢
相關文章
正在查詢...
點晴ERP是一款針對中小制造業的專業生產管理軟件系統,系統成熟度和易用性得到了國內大量中小企業的青睞。
點晴PMS碼頭管理系統主要針對港口碼頭集裝箱與散貨日常運作、調度、堆場、車隊、財務費用、相關報表等業務管理,結合碼頭的業務特點,圍繞調度、堆場作業而開發的。集技術的先進性、管理的有效性于一體,是物流碼頭及其他港口類企業的高效ERP管理信息系統。
點晴WMS倉儲管理系統提供了貨物產品管理,銷售管理,采購管理,倉儲管理,倉庫管理,保質期管理,貨位管理,庫位管理,生產管理,WMS管理系統,標簽打印,條形碼,二維碼管理,批號管理軟件。
點晴免費OA是一款軟件和通用服務都免費,不限功能、不限時間、不限用戶的免費OA協同辦公管理系統。
Copyright 2010-2025 ClickSun All Rights Reserved

黄频国产免费高清视频,久久不卡精品中文字幕一区,激情五月天AV电影在线观看,欧美国产韩国日本一区二区
亚洲中文字幕在线区二 | 亚洲精品动漫卡通在线观看 | 欧美激情精品久久久久久多 | 亚洲大片在线观看完整版 | 亚洲午夜福利片 | 亚洲专区国产精品 |