11

Ifstackalloc与以下引用类型一起使用

var arr = stackalloc string[100];

有错误

无法获取托管类型(“字符串”)的地址、大小或声明指向托管类型的指针

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

4

4 回答 4

8

“问题”更大:在 C# 中,您不能拥有指向托管类型的指针。如果您尝试编写(在 C# 中):

string *pstr;

你会得到:

无法获取托管类型(“字符串”)的地址、大小或声明指向托管类型的指针

现在,stackalloc T[num]返回一个T*(例如参见此处),因此显然stackalloc不能与引用类型一起使用。

您不能拥有指向引用类型的指针的原因可能与 GC 可以在内存中自由移动引用类型(以压缩内存)这一事实有关,因此指针的有效性可能很短。

请注意,在 C++/CLI 中,可以固定引用类型并获取其地址(请参阅pin_ptr

于 2016-02-24T09:29:47.600 回答
6

将 C# 编译器生成的 MSIL 转换为可执行机器代码时,.NET 中的即时编译器执行两项重要任务。显而易见的是生成机器代码。不明显且完全不可见的工作是生成一个表,告诉垃圾收集器在方法执行期间发生 GC 时在哪里查找对象引用。

这是必要的,因为对象根不能仅仅作为类的字段存储在 GC 堆中,还可以存储在局部变量或 CPU 寄存器中。为了正确地完成这项工作,jitter 需要知道堆栈帧的确切结构和存储在那里的变量的类型,以便它可以正确地创建该表。这样,稍后,垃圾收集器可以弄清楚如何读取正确的堆栈帧偏移或 CPU 寄存器来获取对象根值。指向 GC 堆的指针。

这是你使用时的问题stackalloc。该语法利用了允许程序声明自定义值类型的 CLR 功能。围绕正常托管类型声明的后门,限制此值类型不能包含任何字段。只是一个内存块,由程序来为该块生成适当的偏移量。C# 编译器可帮助您根据类型声明和索引表达式生成这些偏移量。

在 C++/CLI 程序中也很常见,相同的自定义值类型特性可以为原生 C++ 对象提供存储。只需要存储该对象的空间,如何正确初始化它并访问该 C++ 对象的成员是 C++ 编译器的一项工作。GC 不需要知道什么。

所以核心限制是没有办法为这个内存块提供类型信息。就 CLR 而言,这些只是没有结构的纯字节,GC 使用的表没有描述其内部结构的选项。

不可避免地,您可以使用的唯一类型是不需要 GC 需要知道的对象引用的类型。Blittable 值类型或指针。所以 System.String 是不行的,它是一个引用类型。您可能得到的最接近“粘稠”的是:

  char** mem = stackalloc char*[100];

进一步的限制是,确保 char* 元素指向固定或非托管字符串完全取决于您。并且您不会索引“数组”超出范围。这不是很实用。

于 2016-02-24T10:37:46.213 回答
1

因为与 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 的安全性要低,但您可以微调您的应用程序并实现高性能 - 低资源应用程序

于 2016-02-24T09:31:26.233 回答
-1

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

于 2016-02-24T09:53:28.763 回答