为什么 stackalloc 不能与引用类型一起使用
本文关键字:一起 引用类型 stackalloc 不能 为什么 | 更新日期: 2023-09-27 18:35:55
如果stackalloc
与引用类型一起使用,如下所示
var arr = stackalloc string[100];
有一个错误
不能获取 的地址、获取其大小或声明指向 托管类型("字符串")
为什么会这样?为什么CLR
不能声明指向托管类型的指针?
将 C# 编译器生成的 MSIL 转换为可执行机器代码时,.NET 中的实时编译器执行两项重要职责。 显而易见和可见的是生成机器代码。 不明显且完全不可见的工作是生成一个表,该表告诉垃圾回收器在执行方法时发生 GC 时在哪里查找对象引用。
这是必要的,因为对象根不能仅仅存储在GC堆中,作为类的字段,而且还存储在局部变量或CPU寄存器中。 为了正确地完成这项工作,抖动需要知道堆栈帧的确切结构以及存储在那里的变量的类型,以便它可以正确创建该表。 这样,垃圾回收器以后就可以弄清楚如何读取正确的堆栈帧偏移量或 CPU 寄存器以获取对象根值。 指向 GC 堆的指针。
当您使用stackalloc
时,这是一个问题。 该语法利用允许程序声明自定义值类型的 CLR 功能。 围绕普通托管类型声明的后门,限制是此值类型不能包含任何字段。 只是一个内存 blob,由程序生成适当的偏移量到该 blob 中。 C# 编译器可帮助您基于类型声明和索引表达式生成这些偏移量。
在 C++/CLI 程序中也很常见,相同的自定义值类型功能可以为本机C++对象提供存储。 只需要存储空间来存储该对象,如何正确初始化它并访问该C++对象的成员是C++编译器解决的工作。 GC 不需要知道的任何内容。
因此,核心限制是无法为此内存 blob 提供类型信息。 就 CLR 而言,这些只是没有结构的普通字节,GC 使用的表没有描述其内部结构的选项。
不可避免地,唯一可以使用的类型是不需要 GC 需要知道的对象引用的类型。 可闪电式值类型或指针。 所以System.String是一个禁忌,它是一个引用类型。 您可能得到的最接近的"字符串"是:
char** mem = stackalloc char*[100];
进一步的限制是,确保 char* 元素指向固定或非托管字符串完全取决于您。 并且您不会将"数组"索引为越界。 这不是很实用。
"问题"更大:在 C# 中,不能有指向托管类型的指针。如果您尝试编写(用 C#):
string *pstr;
您将获得:
无法获取托管类型的地址、获取其大小或声明指向托管类型("字符串")的指针
现在,stackalloc T[num]
返回一个T*
(请参阅此处的示例),因此显然stackalloc
不能与引用类型一起使用。
有指向引用类型的指针的原因可能与以下事实有关:GC 可以在内存中自由移动引用类型(以压缩内存),因此指针的有效性可能很短。
请注意,在 C++/CLI 中,可以固定引用类型并获取其地址(请参阅pin_ptr)
因为 C# 是为了内存安全而进行垃圾回收,而不是C++,所以你应该了解内存管理的新原理。
例如,看看下一个代码:
public static void doAsync(){
var arr = stackalloc string[100];
arr[0] = "hi";
System.Threading.ThreadPool.QueueUserWorkItem(()=>{
Thread.Sleep(10000);
Console.Write(arr[0]);
});
}
该程序将很容易崩溃。 由于arr
是堆栈分配的,因此一旦doAsync
结束,对象 + 它的内存就会消失。 lamda 函数仍然指向这个不再有效的内存地址,这是无效状态。
如果通过引用传递本地基元,则会出现相同的问题。
架构为:
静态对象 ->在整个分配时间
中存在本地对象 -> 只要创建它们的 Scope 有效
,它们就会存在堆分配的对象(使用 new
创建)-只要有人持有对它们的引用,>存在。
另一个问题是垃圾收集在时间段内工作。 当一个对象是本地的时,它应该在函数结束后立即完成,因为在那之后 - 内存将被其他变量覆盖。无论如何,GC 都不能强制完成对象,或者不应该完成对象。
不过,好消息是,C# JIT 有时(并非总是)可以确定可以在堆栈上安全地分配对象,并在可能的情况下(有时再次)诉诸堆栈分配。
另一方面,在C++中,您可以声明所有内容,但这的安全性低于C#或Java,但是您可以微调应用程序并实现高性能 - 低资源应用程序
我认为Xanatos发布了正确的答案。
无论如何,这不是一个答案,而是另一个答案的反例。
请考虑以下代码:
using System;
using System.Threading;
namespace Demo
{
class Program
{
static void Main(string[] args)
{
doAsync();
Thread.Sleep(2000);
Console.WriteLine("Did we finish?"); // Likely this is never displayed.
}
public static unsafe void doAsync()
{
int n = 10000;
int* arr = stackalloc int[n];
ThreadPool.QueueUserWorkItem(x => {
Thread.Sleep(1000);
for (int i = 0; i < n; ++i)
arr[i] = 0;
});
}
}
}
如果运行该代码,它将崩溃,因为堆栈数组是在释放堆栈内存之后
写入的。这表明 stackalloc 不能与引用类型一起使用的原因不仅仅是为了防止此类错误。