在.NET环境中,数组的存储位置对于程序的性能有着显著的影响。数组可以存储在堆内存(heap)或栈内存(stack)上。堆内存是.NET运行时管理的内存区域,而栈内存则由操作系统直接管理。堆内存上的数组由于垃圾回收器的存在,可能会在内存中移动,这会导致性能问题。相反,栈内存上的数组则具有更高的性能,因为它们存储在栈上,直接由CPU访问,无需垃圾回收器的干预。
在.NET中,几乎所有的引用类型都存储在堆内存上。这是因为它们继承自System.Object
类。将对象存储在堆内存意味着它将一直保留在内存中,直到垃圾回收器(无论是自动的还是显式调用System.GC.Collect()
)将其清除。存储在堆内存上的对象会因为CLR(公共语言运行时)存储和检索的开销而导致性能下降。
为了克服性能问题,创建短命的高性能数组或者与非托管代码互操作时,就需要使用栈上的数组。栈上的数组存储在栈内存中,这意味着它们具有高性能和短暂的生命周期,因为绕过了CLR,直接与内存打交道。
值得注意的是,无论是直接还是间接继承自System.Object
的类型都是基于堆的。相反,无论是直接还是间接继承自System.ValueType
的类型都是基于栈的。尽管System.ValueType
继承自System.Object
,但它是基于栈的。所有枚举、结构体和原始数据类型(如Int32
和Boolean
)都是基于栈的类型。
创建栈上的数组非常简单,但首先需要允许项目使用不安全代码,这可以通过项目属性中的构建标签来实现。之后,就可以编写代码了。创建栈上数组的代码如下:
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++;
}
}
}