在 .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> 一起使用时) 否 是(通常需要) 否
详细区别与说明
- 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 自动失效,内存被自动回收- 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);
}- 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);
}
}- 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 类。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用。你还可以使用@来通知其他用户。