堆和栈上的数组及其性能考量

在.NET环境中,数组的存储位置对于程序的性能有着显著的影响。数组可以存储在堆内存(heap)或栈内存(stack)上。堆内存是.NET运行时管理的内存区域,而栈内存则由操作系统直接管理。堆内存上的数组由于垃圾回收器的存在,可能会在内存中移动,这会导致性能问题。相反,栈内存上的数组则具有更高的性能,因为它们存储在栈上,直接由CPU访问,无需垃圾回收器的干预。

堆内存上的数组

在.NET中,几乎所有的引用类型都存储在堆内存上。这是因为它们继承自System.Object类。将对象存储在堆内存意味着它将一直保留在内存中,直到垃圾回收器(无论是自动的还是显式调用System.GC.Collect())将其清除。存储在堆内存上的对象会因为CLR(公共语言运行时)存储和检索的开销而导致性能下降。

栈内存上的数组

为了克服性能问题,创建短命的高性能数组或者与非托管代码互操作时,就需要使用栈上的数组。栈上的数组存储在栈内存中,这意味着它们具有高性能和短暂的生命周期,因为绕过了CLR,直接与内存打交道。

值得注意的是,无论是直接还是间接继承自System.Object的类型都是基于堆的。相反,无论是直接还是间接继承自System.ValueType的类型都是基于栈的。尽管System.ValueType继承自System.Object,但它是基于栈的。所有枚举、结构体和原始数据类型(如Int32Boolean)都是基于栈的类型。

创建栈上的数组

创建栈上的数组非常简单,但首先需要允许项目使用不安全代码,这可以通过项目属性中的构建标签来实现。之后,就可以编写代码了。创建栈上数组的代码如下:

public unsafe static void CreateArray() { int length = 10; // 创建Int32类型的栈上数组,指定长度 int* pArr = stackalloc int[length]; // 设置第一个值为1 *pArr = 1; // 这段代码也将第一个值设置为10 pArr[0] = 10; // 设置第二个值为2 *(pArr + 1) = 2; // 这段代码也将第二个值设置为20 pArr[1] = 20; // 检索存储的值 Console.WriteLine("First value: {0}", *pArr); Console.WriteLine("First value: {0}", pArr[0]); Console.WriteLine("Second value: {0}", *(pArr + 1)); Console.WriteLine("Second value: {0}", pArr[1]); Console.WriteLine(); // Prints: // First value: 10 // First value: 10 // Second value: 20 // Second value: 20 // 设置所有值 for (int idx = 0; idx < length; idx++) { pArr[idx] = idx + 1; // 这也有效 (pArr + idx) = idx + 1; } // 检索所有值 for (int idx = 0; idx < length; idx++) Console.WriteLine("Value {0} = {1}", idx, pArr[idx]); // Prints: // Value 0 = 1 // Value 1 = 2 // ............ // Value 8 = 9 // Value 9 = 10 // 数组在这里从内存中移除 // 因为它声明的范围在这里结束 }

代码解释

首先,使用stackalloc关键字创建数组,指定新数组的长度和类型,这里以Int32为例(可以更改为任何值类型)。因为Int32在内存中占用4字节,所以为数组预留了40字节(长度10乘以Int32大小4)的内存块在栈上。

最后一张图显示了stackalloc返回的指针,它总是指向数组的第一个元素。注意,每个块都是数组的一个元素。在例子中,它是4字节。创建数组后,考虑到最后一张图,有多种方式访问数组元素。

作用域的说明

如果已经理解了作用域是什么以及它如何影响代码流程,那么可能已经知道如何使用两个大括号创建新的作用域。以下是一个示例类:

public class ClassScope { // Scope 1 public void Method1() { // Scope 1.1 { // Scope 1.1.1 { // Scope 1.1.1.1 } { // Scope 1.1.1.2 } } } public void Method2() { // Scope 1.2 if (true) { // Scope 1.2.1 while (true) { // Scope 1.2.1.1 } } } }

快速复制数组

使用指针直接与内存指针工作,绕过CLR,以最快的速度将元素从一个数组复制到另一个数组是非常有用的。以下代码段实现了这一点:

public unsafe static void Copy(int[] src, int srcIdx, int[] dst, int dstIdx, int count) { // 因为普通数组是基于堆的 // 垃圾回收器可能会不时地移动它们 // 所以使用fixed关键字 // 固定它们并告诉垃圾回收 // 在fixed语句关闭之前不要移动它们 fixed (int* pSrc = src, pDst = dst) { // 获取数组第一个元素的指针 int* pSrcIdx = &srcIdx int* pDstIdx = &dstIdx // 确保只复制所需的数量 for (int counter = 0; counter < count; counter++) { // 复制... // 因为Int32是基于栈的 // 它是被复制而不是被引用 pDst[dstIdx] = pSrc[srcIdx]; // 将指针移动到下一个元素 dstIdx++; srcIdx++; } } }
沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485