Почему stackalloc нельзя использовать с ссылочными типами?
Если stackalloc
используется со ссылочными типами, как показано ниже
var arr = stackalloc string[100];
есть ошибка
Невозможно получить адрес, получить размер или объявить указатель на управляемый тип ("строка")
Почему так? Зачем CLR
нельзя объявить указатель на управляемый тип?
4 ответа
"Проблема" больше: в C# у вас не может быть указателя на управляемый тип. Если вы попробуете написать (в C#):
string *pstr;
ты получишь:
Невозможно получить адрес, получить размер или объявить указатель на управляемый тип ("строка")
Сейчас, stackalloc T[num]
возвращает T*
(см. например здесь), так ясно stackalloc
не может использоваться с ссылочными типами.
Причина, по которой у вас нет указателя на ссылочный тип, вероятно, связана с тем фактом, что GC может свободно перемещать ссылочные типы вокруг памяти (для сжатия памяти), поэтому срок действия указателя может быть коротким.
Обратите внимание, что в C++/CLI можно закрепить ссылочный тип и получить его адрес (см. Pin_ptr)
Компилятор Just-In-Time в.NET выполняет две важные обязанности при преобразовании MSIL, сгенерированного компилятором C#, в исполняемый машинный код. Очевидным и видимым является генерирование машинного кода. Неочевидное и полностью невидимое задание - это создание таблицы, которая сообщает сборщику мусора, где искать ссылки на объекты, когда происходит GC во время выполнения метода.
Это необходимо, поскольку корни объектов не могут быть просто сохранены в куче GC как поле класса, но также могут храниться в локальных переменных или регистрах ЦП. Чтобы правильно выполнить эту работу, джиттер должен знать точную структуру кадра стека и типы переменных, хранящихся там, чтобы он мог правильно создать эту таблицу. Чтобы впоследствии сборщик мусора мог выяснить, как считывать правильное смещение кадра стека или регистр ЦП для получения корневого значения объекта. Указатель в кучу GC.
Это проблема, когда вы используете stackalloc
, Этот синтаксис использует функцию CLR, которая позволяет программе объявлять пользовательский тип значения. Задняя дверь вокруг нормальных объявлений управляемого типа с ограничением, что этот тип значения не может содержать никаких полей. Просто капля памяти, программа должна сгенерировать правильные смещения в этом блобе. Компилятор C# помогает вам генерировать эти смещения на основе объявления типа и выражения индекса.
Также очень распространенная в программе C++/CLI, та же самая особенность типа пользовательского значения может обеспечить хранилище для собственного объекта C++. Требуется только место для хранения этого объекта, как правильно инициализировать его и получить доступ к членам этого объекта C++ - это задача, которую вычисляет компилятор C++. Ничего, о чем нужно знать ГК.
Таким образом, основное ограничение заключается в том, что нет способа предоставить информацию о типе для этого объекта памяти. Что касается CLR, то это просто простые байты без структуры, таблица, которую использует GC, не имеет возможности описать свою внутреннюю структуру.
Неизбежно, единственный тип, который вы можете использовать, это тип, который не требует ссылки на объект, о котором GC должен знать. Blittable значения типов или указатели. Таким образом, System.String не используется, это ссылочный тип. Самое близкое, что вы могли бы получить, это "нить" это:
char** mem = stackalloc char*[100];
С дополнительным ограничением, вы должны убедиться, что элементы char* указывают на закрепленную или неуправляемую строку. И что вы не индексируете "массив" вне границ. Это не очень практично.
Поскольку 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 по-прежнему указывает на этот недействительный адрес памяти, и это недопустимое состояние.
если вы передадите локальные примитивы по ссылке, возникнет та же проблема.
Схема такая:
статические объекты -> живут в течение всего времени размещения
локальный объект -> живет до тех пор, пока сфера, создавшая их, действительна
выделенные кучей объекты (созданные с new
) -> существовать до тех пор, пока кто-то на них ссылается.
Другая проблема в том, что сборка мусора работает по периодам. когда объект является локальным, он должен быть завершен, как только функция будет завершена, потому что по истечении этого времени память будет переопределена другими переменными. GC не может быть принудительно завершен объект, или не должен, в любом случае.
Хорошая вещь, однако, состоит в том, что JIT C# иногда (не всегда) может определить, что объект может быть безопасно размещен в стеке, и будет прибегать к выделению стека, если это возможно (опять же, иногда).
В C++, с другой стороны, вы можете объявить все где угодно, но это обеспечивает меньшую безопасность, чем C# или Java, но вы можете точно настроить свое приложение и добиться высокой производительности - приложения с низким уровнем ресурсов
Я думаю, что Ксанатос отправил правильный ответ.
В любом случае, это не ответ, а контрпример к другому ответу.
Рассмотрим следующий код:
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 не может использоваться с ссылочными типами, заключается не просто в предотвращении такого рода ошибок.