为什么 stackalloc 不能与引用类型一起使用?
Why stackalloc cannot be used with reference types?
如果 stackalloc
与引用类型一起使用,如下所示
var arr = stackalloc string[100];
有错误
Cannot take the address of, get the size of, or declare a pointer to a
managed type ('string')
为什么会这样?为什么 CLR
不能声明指向托管类型的指针?
"problem" 更大:在 C# 中,您不能拥有指向托管类型的指针。如果您尝试编写(在 C# 中):
string *pstr;
你会得到:
Cannot take the address of, get the size of, or declare a pointer to a managed type ('string')
现在,stackalloc T[num]
returns a T*
(参见 here),很明显 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不能与引用类型一起使用的原因不仅仅是为了防止这种错误。
.NET 中的即时编译器在将 C# 编译器生成的 MSIL 转换为 executable 机器代码时执行两项重要任务。显而易见的是生成机器代码。不明显且完全不可见的工作是生成一个 table 告诉垃圾收集器在方法执行时发生 GC 时到哪里查找对象引用。
这是必要的,因为对象根不能只存储在 GC 堆中,作为 class 的字段,但 也 存储在局部变量或 CPU 寄存器。为了正确地完成这项工作,抖动需要知道堆栈帧的确切结构和存储在那里的变量的类型,以便它可以正确地创建 table。这样,稍后垃圾收集器就可以弄清楚如何读取正确的堆栈帧偏移量或 CPU 寄存器来获取对象根值。指向 GC 堆的指针。
使用 stackalloc
时会出现问题。该语法利用了允许程序声明自定义值类型的 CLR 功能。围绕正常托管类型声明的后门,限制是此值类型不能包含任何字段。只是一块内存,由程序生成适当的偏移量到该块中。 C# 编译器可帮助您根据类型声明和索引表达式生成这些偏移量。
在 C++/CLI 程序中也很常见,相同的自定义值类型功能可以为本机 C++ 对象提供存储。只需要 space 用于存储该对象,如何正确初始化它并访问该 C++ 对象的成员是 C++ 编译器解决的工作。 GC 无需了解任何内容。
所以核心限制是无法为这块内存提供类型信息。就 CLR 而言,这些只是没有结构的普通字节,GC 使用的 table 没有选项来描述其内部结构。
不可避免地,您唯一可以使用的类型是不需要 GC 需要了解的对象引用的类型。 Blittable 值类型或指针。所以 System.String 不行,它是一个引用类型。您可能得到的最接近的 "stringy" 是:
char** mem = stackalloc char*[100];
进一步的限制是,确保 char* 元素指向固定或非托管字符串完全取决于您。而且你没有索引 "array" 越界。这个不太实用
如果 stackalloc
与引用类型一起使用,如下所示
var arr = stackalloc string[100];
有错误
Cannot take the address of, get the size of, or declare a pointer to a managed type ('string')
为什么会这样?为什么 CLR
不能声明指向托管类型的指针?
"problem" 更大:在 C# 中,您不能拥有指向托管类型的指针。如果您尝试编写(在 C# 中):
string *pstr;
你会得到:
Cannot take the address of, get the size of, or declare a pointer to a managed type ('string')
现在,stackalloc T[num]
returns a T*
(参见 here),很明显 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不能与引用类型一起使用的原因不仅仅是为了防止这种错误。
.NET 中的即时编译器在将 C# 编译器生成的 MSIL 转换为 executable 机器代码时执行两项重要任务。显而易见的是生成机器代码。不明显且完全不可见的工作是生成一个 table 告诉垃圾收集器在方法执行时发生 GC 时到哪里查找对象引用。
这是必要的,因为对象根不能只存储在 GC 堆中,作为 class 的字段,但 也 存储在局部变量或 CPU 寄存器。为了正确地完成这项工作,抖动需要知道堆栈帧的确切结构和存储在那里的变量的类型,以便它可以正确地创建 table。这样,稍后垃圾收集器就可以弄清楚如何读取正确的堆栈帧偏移量或 CPU 寄存器来获取对象根值。指向 GC 堆的指针。
使用 stackalloc
时会出现问题。该语法利用了允许程序声明自定义值类型的 CLR 功能。围绕正常托管类型声明的后门,限制是此值类型不能包含任何字段。只是一块内存,由程序生成适当的偏移量到该块中。 C# 编译器可帮助您根据类型声明和索引表达式生成这些偏移量。
在 C++/CLI 程序中也很常见,相同的自定义值类型功能可以为本机 C++ 对象提供存储。只需要 space 用于存储该对象,如何正确初始化它并访问该 C++ 对象的成员是 C++ 编译器解决的工作。 GC 无需了解任何内容。
所以核心限制是无法为这块内存提供类型信息。就 CLR 而言,这些只是没有结构的普通字节,GC 使用的 table 没有选项来描述其内部结构。
不可避免地,您唯一可以使用的类型是不需要 GC 需要了解的对象引用的类型。 Blittable 值类型或指针。所以 System.String 不行,它是一个引用类型。您可能得到的最接近的 "stringy" 是:
char** mem = stackalloc char*[100];
进一步的限制是,确保 char* 元素指向固定或非托管字符串完全取决于您。而且你没有索引 "array" 越界。这个不太实用