动态

详情 返回 返回

.netcore中的內存分配有哪些?它們之間有什麼區別? - 动态 详情

在 .NET 中,提供高性能、非託管或可控內存分配的方式主要有以下幾種,但它們之間存在關鍵區別:

stackalloc

ArrayPool<T>.Shared

Span<T> / Memory<T> (通常與上述方式結合使用)

NativeMemory 類 (用於本地內存分配)

Marshal 類 (特別是 AllocHGlobal 和 CoTaskMemAlloc)

下面我們來詳細解釋它們之間的區別。
對比總結表
特性 stackalloc ArrayPool<T>.Shared NativeMemory / Marshal 常規 new T[]
內存位置 棧(Stack) 託管堆(預先分配的大數組) 非託管堆(Unmanaged Heap) 託管堆(Managed Heap)
內存管理 自動(方法返回時釋放) 手動租借/歸還(池) 手動(必須顯式釋放) 自動(GC 回收)
安全風險 棧溢出(大內存) 無(池化管理) 內存泄漏(若忘記釋放) 無
大小限制 很小(約 1 MB,取決於棧深度) 很大(通常可達 1GB) 很大(受系統內存限制) 很大(受 GC 和系統限制)
性能 極高(無分配壓力,無GC) 高(避免GC,複用數組) 高(但分配/釋放成本高於棧) 較低(有GC壓力)
適用範圍 小型的、短暫的緩衝區 中大型的、頻繁使用的臨時緩衝區 與本地代碼互操作、需要精確控制的大型內存 通用的、長期存在的數組
返回類型 Span<T> (C# 7.2+) T[] 或 ArraySegment<T> void* 或 IntPtr T[]
需要不安全上下文 是(C# 7.2 前);否(與 Span<T> 一起使用時) 否 是(通常需要) 否
詳細區別與説明

  1. stackalloc
    核心特點:在棧上分配內存塊。棧內存的分配和釋放速度極快,因為它只是移動棧指針。

優點:完全沒有垃圾回收(GC)壓力,性能極致。

缺點:

棧空間非常有限。默認棧大小通常為 1-4 MB,分配過大內存(如 stackalloc int[100000])極易導致棧溢出(StackOverflowException),且此異常無法被捕獲,會導致進程立即終止。

分配的內存的生命週期嚴格限定在所在方法的作用域內。方法返回後,內存自動失效。

適用場景:極其注重性能的熱路徑代碼,且需要分配的緩衝區非常小(例如,處理一個算法中的臨時數組,大小在幾百字節到幾KB之間)。

示例:

// C# 7.2+ 後,可以與 Span<T> 安全地一起使用,無需 unsafe 關鍵字
Span<int> buffer = stackalloc int[128];
for (int i = 0; i < buffer.Length; i++)
{
    buffer[i] = i;
}
// 方法結束時,buffer 自動失效,內存被自動回收
  1. ArrayPool<T>.Shared
    核心特點:它是一個託管數組的對象池。你從池中“租借”(Rent)一個數組,用完後“歸還”(Return)給它。池會緩存這些數組以供後續複用。

優點:

避免GC:通過複用數組,大大減少了託管堆上的分配和垃圾回收次數。

安全:沒有棧溢出風險。

可分配較大內存:池中的數組可以很大。

缺點:

需要手動管理:必須記得調用 Return 方法歸還數組,否則失去池化的意義,甚至可能導致問題(例如,租借的數組可能包含舊數據)。

性能略低於 stackalloc,因為它仍然涉及託管堆上的一個數組對象,但遠優於每次都 new T[]。

適用場景:需要頻繁創建和銷燬的中大型臨時數組或緩衝區(例如,網絡IO、文件流處理中的緩衝區)。

示例:

using System.Buffers;

// 從共享池租借一個最小長度為 1024 的數組
int[] largeBuffer = ArrayPool<int>.Shared.Rent(1024);

try
{
    // 使用 largeBuffer
    // 注意:Rent 返回的數組長度可能大於你請求的長度!
    var usableSpan = largeBuffer.AsSpan(0, 1024);
    // ... 處理 usableSpan
}
finally
{
    // 務必在完成後歸還數組
    ArrayPool<int>.Shared.Return(largeBuffer);
}
  1. NativeMemory / Marshal 類
    核心特點:在非託管堆上分配原生內存塊。這部分內存完全在 GC 的管轄範圍之外。

優點:

內存大小不受 GC 約束,生命週期完全由開發者控制。

與本地代碼互操作的必備手段(例如,為 P/Invoke 調用準備結構體)。

缺點:

必須手動釋放!忘記調用對應的 Free 方法會導致內存泄漏。

使用指針,通常需要 unsafe 上下文,增加了代碼的複雜性。

分配和釋放成本高於棧,低於或近似於託管堆。

適用場景:

與本地 API 進行互操作。

需要分配非常大且生命週期很長、不希望給 GC 帶來壓力的內存塊。

示例:

using System.Runtime.InteropServices;

unsafe
{
    // 使用 NativeMemory (在 .NET 6+ 中更推薦)
    int* nativeBuffer = (int*)NativeMemory.Alloc(100, sizeof(int));

    // 或者使用傳統的 Marshal
    // IntPtr nativeBufferPtr = Marshal.AllocHGlobal(100 * sizeof(int));
    // int* nativeBuffer = (int*)nativeBufferPtr;

    try
    {
        // 使用 nativeBuffer
        for (int i = 0; i < 100; i++)
        {
            nativeBuffer[i] = i;
        }
    }
    finally
    {
        // 必須手動釋放!
        NativeMemory.Free(nativeBuffer);
        // Marshal.FreeHGlobal(nativeBufferPtr);
    }
}
  1. Span<T> 和 Memory<T> 的作用
    需要特別注意的是,Span<T> 和 Memory<T> 本身不是內存分配機制,而是提供了一種統一、安全的方式來訪問各種背襯存儲(Backing Store)上的連續內存。

Span<T>:可以指向 stackalloc 的內存、ArrayPool 的數組、常規 new 的數組、非託管內存等。它是 ref struct,所以只能存在於棧上,這使得它能安全地指向棧內存。

Memory<T>:類似於 Span<T>,但它不是 ref struct,所以可以放在堆上(例如,用於異步方法)。它不能指向棧內存(如 stackalloc 的結果)。

它們是將上述各種分配方式與現代 .NET 代碼連接起來的橋樑,讓你能用相似的 API 操作不同來源的內存。

總結與選擇建議
你的需求 推薦方案
極小(KB級)、短暫、極致性能的緩衝區 stackalloc(務必確保尺寸很小!)
頻繁使用的中大型臨時緩衝區 ArrayPool<T>.Shared(首選,安全高效)
與本地代碼交互或完全控制生命週期的大型內存 NativeMemory / Marshal(記得手動釋放)
普通用途的數組 new T[](最簡單,但GC有壓力)
簡單來説,stackalloc 是性能最高但限制最大的特殊工具。在大多數需要優化臨時緩衝區分配的場景下,ArrayPool<T>.Shared 是更通用、更安全的選擇。而與非託管世界打交道時,則必須使用 NativeMemory 或 Marshal 類。

user avatar evans_bo 头像 mstech 头像 chuanghongdengdeqingwa_eoxet2 头像 tangqingfeng 头像
点赞 4 用户, 点赞了这篇动态!
点赞

Add a new 评论

Some HTML is okay.