Распределяет ли использование «new» в структуре его в куче или стеке?

290

Когда вы создаете экземпляр класса с newоператором, память выделяется в куче. Когда вы создаете экземпляр структуры с newоператором, где выделяется память, в куче или в стеке?

Кедар Камте
источник

Ответы:

306

Хорошо, давайте посмотрим, смогу ли я сделать это более понятным.

Во-первых, Эш прав: вопрос не в том, где расположены переменные типа значения . Это другой вопрос, на который ответ не просто «в стеке». Это сложнее, чем это (и стало еще сложнее в C # 2). У меня есть статья на эту тему, и я буду расширять ее, если потребуется, но давайте разберемся только с newоператором.

Во-вторых, все это действительно зависит от того, на каком уровне вы говорите. Я смотрю на то, что компилятор делает с исходным кодом, с точки зрения IL, который он создает. Более чем возможно, что JIT-компилятор сделает умные вещи с точки зрения оптимизации большого количества «логического» распределения.

В-третьих, я игнорирую дженерики, в основном потому, что на самом деле не знаю ответа, а отчасти потому, что это слишком усложнит ситуацию.

Наконец, все это только с текущей реализацией. Спецификация C # не определяет многое из этого - это фактически деталь реализации. Есть те, кто считает, что разработчикам управляемого кода на самом деле все равно. Я не уверен, что зашел бы так далеко, но стоит представить мир, где на самом деле все локальные переменные живут в куче - что все равно будет соответствовать спецификации.


newОператор для типов значений может иметь две разные ситуации : вы можете вызвать конструктор без параметров (например new Guid()) или конструктор с параметрами (например new Guid(someString)). Они генерируют существенно разные IL. Чтобы понять почему, вам нужно сравнить спецификации C # и CLI: в соответствии с C # все типы значений имеют конструктор без параметров. Согласно спецификации CLI, типы значений не имеют конструкторов без параметров. (Получить конструкторы типа значения с отражением некоторое время - вы не найдете один без параметров.)

Это имеет смысл для C # , чтобы лечить «инициализировать значение нулями» как конструктор, потому что он держит язык соответствует - вы можете думать , new(...)как всегда вызов конструктора. Для CLI имеет смысл думать об этом по-другому, так как нет реального кода для вызова - и, конечно, нет кода для конкретного типа.

Также имеет значение, что вы собираетесь делать со значением после его инициализации. IL используется для

Guid localVariable = new Guid(someString);

отличается от IL, используемого для:

myInstanceOrStaticVariable = new Guid(someString);

Кроме того, если значение используется в качестве промежуточного значения, например, в качестве аргумента для вызова метода, все снова немного меняется. Чтобы показать все эти различия, вот небольшая тестовая программа. Это не показывает разницу между статическими переменными и переменными экземпляра: IL будет отличаться между stfldи stsfld, но это все.

using System;

public class Test
{
    static Guid field;

    static void Main() {}
    static void MethodTakingGuid(Guid guid) {}


    static void ParameterisedCtorAssignToField()
    {
        field = new Guid("");
    }

    static void ParameterisedCtorAssignToLocal()
    {
        Guid local = new Guid("");
        // Force the value to be used
        local.ToString();
    }

    static void ParameterisedCtorCallMethod()
    {
        MethodTakingGuid(new Guid(""));
    }

    static void ParameterlessCtorAssignToField()
    {
        field = new Guid();
    }

    static void ParameterlessCtorAssignToLocal()
    {
        Guid local = new Guid();
        // Force the value to be used
        local.ToString();
    }

    static void ParameterlessCtorCallMethod()
    {
        MethodTakingGuid(new Guid());
    }
}

Вот IL для класса, исключая нерелевантные биты (такие как nops):

.class public auto ansi beforefieldinit Test extends [mscorlib]System.Object    
{
    // Removed Test's constructor, Main, and MethodTakingGuid.

    .method private hidebysig static void ParameterisedCtorAssignToField() cil managed
    {
        .maxstack 8
        L_0001: ldstr ""
        L_0006: newobj instance void [mscorlib]System.Guid::.ctor(string)
        L_000b: stsfld valuetype [mscorlib]System.Guid Test::field
        L_0010: ret     
    }

    .method private hidebysig static void ParameterisedCtorAssignToLocal() cil managed
    {
        .maxstack 2
        .locals init ([0] valuetype [mscorlib]System.Guid guid)    
        L_0001: ldloca.s guid    
        L_0003: ldstr ""    
        L_0008: call instance void [mscorlib]System.Guid::.ctor(string)    
        // Removed ToString() call
        L_001c: ret
    }

    .method private hidebysig static void ParameterisedCtorCallMethod() cil  managed    
    {   
        .maxstack 8
        L_0001: ldstr ""
        L_0006: newobj instance void [mscorlib]System.Guid::.ctor(string)
        L_000b: call void Test::MethodTakingGuid(valuetype [mscorlib]System.Guid)
        L_0011: ret     
    }

    .method private hidebysig static void ParameterlessCtorAssignToField() cil managed
    {
        .maxstack 8
        L_0001: ldsflda valuetype [mscorlib]System.Guid Test::field
        L_0006: initobj [mscorlib]System.Guid
        L_000c: ret 
    }

    .method private hidebysig static void ParameterlessCtorAssignToLocal() cil managed
    {
        .maxstack 1
        .locals init ([0] valuetype [mscorlib]System.Guid guid)
        L_0001: ldloca.s guid
        L_0003: initobj [mscorlib]System.Guid
        // Removed ToString() call
        L_0017: ret 
    }

    .method private hidebysig static void ParameterlessCtorCallMethod() cil managed
    {
        .maxstack 1
        .locals init ([0] valuetype [mscorlib]System.Guid guid)    
        L_0001: ldloca.s guid
        L_0003: initobj [mscorlib]System.Guid
        L_0009: ldloc.0 
        L_000a: call void Test::MethodTakingGuid(valuetype [mscorlib]System.Guid)
        L_0010: ret 
    }

    .field private static valuetype [mscorlib]System.Guid field
}

Как видите, для вызова конструктора используется множество различных инструкций:

  • newobj: Распределяет значение в стеке, вызывает параметризованный конструктор. Используется для промежуточных значений, например, для присвоения полю или использования в качестве аргумента метода.
  • call instance: Использует уже выделенное место хранения (в стеке или нет). Это используется в приведенном выше коде для присвоения локальной переменной. Если одной и той же локальной переменной присваивается значение несколько раз с использованием нескольких newвызовов, она просто инициализирует данные поверх старого значения - она не выделяет больше места в стеке каждый раз.
  • initobj: Использует уже выделенное место хранения и просто стирает данные. Это используется для всех наших вызовов конструктора без параметров, включая те, которые присваиваются локальной переменной. Для вызова метода эффективно вводится промежуточная локальная переменная, а ее значение стирается initobj.

Я надеюсь, что это показывает, насколько сложна тема, и в то же время проливает немного света на нее. В некоторых концептуальных смыслах каждый вызов newвыделяет пространство в стеке - но, как мы видели, это не то, что действительно происходит даже на уровне IL. Я хотел бы выделить один конкретный случай. Возьми этот метод:

void HowManyStackAllocations()
{
    Guid guid = new Guid();
    // [...] Use guid
    guid = new Guid(someBytes);
    // [...] Use guid
    guid = new Guid(someString);
    // [...] Use guid
}

Это «логически» имеет 4 выделения стека - по одному для переменной и по одному для каждого из трех newвызовов - но на самом деле (для этого конкретного кода) стек выделяется только один раз, а затем то же место хранения используется повторно.

РЕДАКТИРОВАТЬ: Просто чтобы быть ясно, это верно только в некоторых случаях ... в частности, значение guidне будет видно, если Guidконструктор выдает исключение, поэтому компилятор C # может повторно использовать тот же слот стека. См. Сообщение Эрика Липперта в блоге о создании типа значения для получения дополнительной информации и случая, когда оно не применяется.

Я многому научился писать этот ответ - пожалуйста, попросите разъяснений, если что-то неясно!

Джон Скит
источник
1
Джон, пример кода HowManyStackAllocations хорош. Но не могли бы вы изменить его на использование Struct вместо Guid, или добавить новый пример Struct. Я думаю, что тогда он напрямую затронет оригинальный вопрос @ kedar.
Пепел
9
Guid - это уже структура. См. Msdn.microsoft.com/en-us/library/system.guid.aspx. Я бы не выбрал тип ссылки для этого вопроса :)
Джон Скит,
1
Что происходит, когда у вас есть List<Guid>и добавить эти 3 к нему? Это было бы 3 распределения (тот же IL)? Но они хранятся где-то волшебно
Арек Баррвин
1
@Ani: Вам не хватает того факта, что в примере Эрика есть блок try / catch - поэтому, если во время конструктора структуры выдается исключение, вы должны иметь возможность увидеть значение перед конструктором. Мой пример не такая ситуация - если конструктор не может с исключением, это не имеет значения , если значение guidимеет только наполовину перезаписаны, так как он не будет виден в любом случае.
Джон Скит
2
@Ani: На самом деле, Эрик называет это в нижней части своего поста: «А как насчет точки Веснера? Да, на самом деле, если это локальная переменная, выделенная в стеке (а не поле в замыкании), которая объявлена на том же уровне вложенности «try», что и вызов конструктора, тогда мы не пройдем через этот ригамарол создания нового временного объекта, инициализации временного и копирования его в локальный. В этом конкретном (и общем) случае мы можем оптимизировать создание временной копии и ее копии, поскольку для программы на C # невозможно заметить разницу! "
Джон Скит
40

Память, содержащая поля структуры, может быть выделена либо в стеке, либо в куче, в зависимости от обстоятельств. Если переменная типа структуры является локальной переменной или параметром, который не захвачен каким-либо анонимным классом делегата или итератора, то он будет размещен в стеке. Если переменная является частью некоторого класса, то она будет размещена внутри класса в куче.

Если структура размещена в куче, то для выделения памяти фактически не нужно вызывать оператор new. Единственная цель - установить значения полей в соответствии с тем, что находится в конструкторе. Если конструктор не вызывается, то все поля получат значения по умолчанию (0 или ноль).

Точно так же для структур, размещенных в стеке, за исключением того, что C # требует, чтобы все локальные переменные были установлены в какое-то значение перед их использованием, поэтому вы должны вызвать либо пользовательский конструктор, либо конструктор по умолчанию (конструктор, который не принимает параметров, всегда доступен структуры).

Джеффри Л Уитледж
источник
13

Короче говоря, new - это неправильное выражение для структур, вызов new просто вызывает конструктор. Единственным местом хранения структуры является место, где она определена.

Если это переменная-член, она сохраняется непосредственно в том месте, в котором она определена, если это локальная переменная или параметр, она хранится в стеке.

Сравните это с классами, которые имеют ссылку, где бы структура не хранилась целиком, тогда как ссылки указывают где-то в куче. (Член внутри, локальный / параметр в стеке)

Это может помочь немного заглянуть в C ++, где нет реального различия между классом / структурой. (Есть похожие имена в языке, но они относятся только к доступности по умолчанию вещей). Когда вы вызываете new, вы получаете указатель на местоположение кучи, в то время как если у вас есть ссылка без указателя, она сохраняется непосредственно в стеке или в другом объекте ала строит в C #.

Guvante
источник
5

Как и для всех типов значений, структуры всегда идут туда, где они были объявлены .

Смотрите этот вопрос здесь для более подробной информации о том, когда использовать структуры. И этот вопрос здесь для дополнительной информации о структурах.

Редактировать: я ответил, что они ВСЕГДА идут в стек. Это неправильно .

Эстебан Арайя
источник
«структуры всегда идут туда, где их объявили», это немного сбивает с толку. Поле структуры в классе всегда помещается в «динамическую память, когда создается экземпляр типа» - Джефф Рихтер. Это может быть косвенно в куче, но это совсем не то же самое, что обычный тип ссылки.
Пепел
Нет, я думаю, что это совершенно правильно - хотя это не то же самое, что ссылочный тип. Значение переменной живет там, где она объявлена. Значение переменной ссылочного типа является ссылкой, а не фактическими данными, вот и все.
Джон Скит
Таким образом, всякий раз, когда вы создаете (объявляете) тип значения в любом месте метода, он всегда создается в стеке.
Пепел
2
Джон, ты упустил мою точку зрения. Причина, по которой этот вопрос был впервые задан, заключается в том, что многим разработчикам (включая меня, пока я не прочитал CLR Via C #) неясно, где размещается структура, если для ее создания используется оператор new. Сказать, что «структуры всегда идут туда, где их объявили», не является четким ответом.
Пепел
1
@Ash: если у меня будет время, я постараюсь написать ответ, когда доберусь до работы. Это слишком большая тема, чтобы пытаться ее охватить в поезде :)
Джон Скит
4

Я, наверное, что-то здесь упускаю, но почему мы заботимся о распределении?

Типы значений передаются по значению;) и поэтому не могут быть видоизменены в другой области видимости, чем те, в которых они определены. Чтобы иметь возможность изменять значение, вы должны добавить ключевое слово [ref].

Ссылочные типы передаются по ссылке и могут быть изменены.

Конечно, есть строки неизменяемых ссылочных типов, которые являются наиболее популярными.

Расположение / инициализация массива: Типы значений -> нулевая память [имя, zip] [имя, zip] Типы ссылок -> нулевая память -> null [ref] [ref]

user18579
источник
3
Ссылочные типы не передаются по ссылке - ссылки передаются по значению. Это очень разные.
Джон Скит
2

Объявление classor structпохоже на план, который используется для создания экземпляров или объектов во время выполнения. Если вы определяете classили structвызываете Person, Person - это имя типа. Если вы объявляете и инициализируете переменную p типа Person, p называется объектом или экземпляром Person. Можно создать несколько экземпляров одного и того же типа Person, и каждый экземпляр может иметь разные значения в своих propertiesи fields.

А classявляется ссылочным типом. Когда объект classсоздается, переменная, которой назначен объект, содержит только ссылку на эту память. Когда ссылка на объект назначается новой переменной, новая переменная ссылается на исходный объект. Изменения, сделанные с помощью одной переменной, отражаются в другой переменной, поскольку оба они ссылаются на одни и те же данные.

А structявляется типом значения. Когда создается a struct, переменная, которой structназначен, содержит фактические данные структуры. Когда structобъект назначен новой переменной, он копируется. Поэтому новая переменная и исходная переменная содержат две отдельные копии одних и тех же данных. Изменения, внесенные в одну копию, не влияют на другую копию.

Обычно classesиспользуются для моделирования более сложного поведения или данных, которые предназначены для изменения после создания classобъекта. Structsлучше всего подходят для небольших структур данных, которые содержат в основном данные, которые не предназначены для изменения после structсоздания.

для большего...

Sujit
источник
1

В значительной степени структуры, которые считаются типами значений, размещаются в стеке, в то время как объекты распределяются в куче, а ссылка на объект (указатель) распределяется в стеке.

bashmohandes
источник
1

Структуры распределяются по стеку. Вот полезное объяснение:

Структуры

Кроме того, классы при создании экземпляра в .NET выделяют память в куче или зарезервированном пространстве памяти .NET. Принимая во внимание, что структуры приводят к большей эффективности при реализации из-за распределения в стеке. Кроме того, следует отметить, что передаваемые параметры внутри структур делаются по значению.

DaveK
источник
5
Это не распространяется на случай, когда структура является частью класса - в этот момент она живет в куче вместе с остальными данными объекта.
Джон Скит
1
Да, но на самом деле он фокусируется и отвечает на вопрос, который задают. Проголосовал.
Пепел
... все еще будучи неправильным и вводящим в заблуждение. Извините, но нет коротких ответов на этот вопрос - Джеффри - единственный полный ответ.
Марк Гравелл