为什么 stackalloc 不能与引用类型一起使用

本文关键字:一起 引用类型 stackalloc 不能 为什么 | 更新日期: 2023-09-27 18:35:55

如果stackalloc与引用类型一起使用,如下所示

var arr = stackalloc string[100];

有一个错误

不能获取 的地址、获取其大小或声明指向 托管类型("字符串")

为什么会这样?为什么CLR不能声明指向托管类型的指针?

为什么 stackalloc 不能与引用类型一起使用

将 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 不能与引用类型一起使用的原因不仅仅是为了防止此类错误。