Чи використовує "new" на структурі розподілення його на купі чи стеку?


290

Коли ви створюєте екземпляр класу з newоператором, пам'ять виділяється на купі. Коли ви створюєте екземпляр структури з newоператором, куди виділяється пам'ять, на купі чи на стеку?

Відповіді:


306

Гаразд, давайте подивимось, чи можу я зробити це ясніше.

По-перше, Еш правий: питання не в тому, де розподіляються змінні типу значень . Це вже інше питання - і те, на яке відповідь не просто "на стеці". Це складніше, ніж це (і ще більше ускладнюється C # 2). У мене є стаття на цю тему, і я розширюю її, якщо вимагатиме, але давайте розберемося лише з newоператором.

По-друге, все це насправді залежить від того, про який рівень ви говорите. Я дивлюся, що компілятор робить із вихідним кодом, з точки зору IL, який він створює. Більш ніж можливо, що компілятор JIT зробить розумні речі з точки зору оптимізації досить "логічного" виділення.

По-третє, я ігнорую дженерики, здебільшого тому, що насправді не знаю відповіді, а частково тому, що це занадто ускладнить речі.

Нарешті, все це якраз із поточною реалізацією. Специфікація C # не вказує багато цього - це фактично деталі реалізації. Є такі, хто вважає, що розробникам керованого коду насправді не варто хвилювати. Я не впевнений, що хотів би так далеко, але варто уявити собі світ, де насправді всі локальні змінні живуть на купі - який би все-таки відповідав специфікації.


Існує дві різні ситуації з newоператором щодо типів значень: ви можете викликати конструктор без параметрів (наприклад new Guid()) або конструктор параметрів (наприклад new Guid(someString)). Вони генерують значно різні ІЛ. Щоб зрозуміти чому, вам потрібно порівняти специфікації C # і CLI: згідно з C #, всі типи значень мають конструктор без параметрів. Відповідно до специфікації CLI, жодні типи значень не мають конструкторів без параметрів. (Виконайте конструктор типу значення з відображенням деякий час - ви не знайдете його без параметрів.)

Для C # має сенс трактувати "ініціалізувати значення з нулями" як конструктор, тому що він підтримує мову послідовною - можна подумати, new(...)як завжди викликати конструктор. CLI має сенс думати про це по-різному, оскільки немає справжнього коду для виклику - і, звичайно, немає коду, характерного для типу.

Це також має значення, що ви збираєтеся робити зі значенням після його ініціалізації. IL використовували для

Guid localVariable = new Guid(someString);

відрізняється від ІР, що використовується для:

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виділяє простір на стеку - але, як ми бачили, це не те, що відбувається насправді навіть на рівні ІЛ. Я хотів би виділити один конкретний випадок. Візьміть цей метод:

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

Цей "логічно" має 4 виділення стека - один для змінної та один для кожного з трьох newвикликів - але насправді (для цього конкретного коду) стек виділяється лише один раз, а потім те саме місце зберігання використовується повторно.

EDIT: Просто для того, щоб було зрозуміло, це справедливо лише в деяких випадках ... зокрема, значення guidне буде видно, якщо Guidконструктор викине виняток, саме тому компілятор C # може повторно використовувати той самий слот стека. Докладнішу інформацію див. У блозі Еріка Ліпперта про створення типу вартості для отримання детальної інформації та випадку, коли воно не застосовується.

Я багато чого навчився писати цю відповідь - будь ласка, попросіть роз'яснення, якщо щось із них незрозуміле!


1
Джон, приклад коду HowManyStackAllocations хороший. Але чи можете ви або змінити його на використання Struct замість Guid, або додати новий приклад Struct. Я думаю, що це безпосередньо стосуватиметься оригінального питання @ kedar.
Еш

9
Керівництво - це вже структура. Дивіться msdn.microsoft.com/en-us/library/system.guid.aspx Я б не вибрав тип посилання для цього питання :)
Джон Скіт

1
Що відбувається, коли у вас є, List<Guid>і додайте до них ці 3? Це були б 3 виділення (та сама ІЛ)? Але вони зберігаються десь магічними
Арек Барвін

1
@Ani: Вам не вистачає факту, що в прикладі Еріка є блок try / catch - тому, якщо виняток буде викинуто під час конструктора struct, ви повинні мати можливість бачити значення перед конструктором. У моєму прикладі не буває такої ситуації - якщо конструктор не спрацьовує з винятком, не має значення, якщо значення guidбуло лише перезаписано, оскільки воно все одно не буде видно.
Джон Скіт

2
@Ani: Насправді Ерік називає це внизу своєї посади: "Тепер, що з точкою Веснера? Так, насправді, якщо це оголошена стеком локальна змінна (а не поле в закритті), яке оголошується на тому самому рівні "спробувати" вкладення, як виклик конструктора, тоді ми не проходимо цю ригамаролу створення нового тимчасового, ініціалізації тимчасового та копіювання його до локального. У цьому конкретному (і звичайному) випадку ми можемо оптимізувати створення тимчасової та копії, оскільки програма C # не може помітити різницю! "
Джон Скіт

40

Пам'ять, що містить поля структури, може бути розподілена як у стеці, так і в купі залежно від обставин. Якщо змінна type-типу - це локальна змінна або параметр, який не захоплюється деяким анонімним делегатом або класом ітератора, то він буде виділений у стеці. Якщо змінна є частиною якогось класу, то вона буде розподілена всередині класу на купі.

Якщо структура розподілена на купі, то для виклику нового оператора фактично не потрібно виділяти пам'ять. Єдиною метою було б встановити значення поля відповідно до того, що є в конструкторі. Якщо конструктор не викликається, то всі поля отримають свої значення за замовчуванням (0 або null).

Аналогічно і для структур, виділених у стеку, за винятком того, що C # вимагає, щоб усі локальні змінні були встановлені на якесь значення перед їх використанням, тому вам доведеться викликати або власний конструктор, або конструктор за замовчуванням (конструктор, який не приймає жодних параметрів, завжди доступний для конструкції).


13

Простіше кажучи, нове - це неправильне значення для структур, виклик нового просто викликає конструктор. Єдине місце зберігання структури - це визначене місце.

Якщо це змінна-член, вона зберігається безпосередньо в тому, що вона визначена, якщо вона є локальною змінною або параметром, вона зберігається в стеці.

Протиставляйте це класам, які мають посилання, де структура зберігалася б у повному обсязі, тоді як опорні точки десь на купі. (Учасник, локальний / параметр у стеці)

Це може допомогти трохи заглянути в C ++, де немає різниці між класом / структурою. (У мові є подібні імена, але вони стосуються лише доступності речей за замовчуванням) Коли ви зателефонуєте до нового, ви отримаєте вказівник на місце купи, тоді як, якщо у вас є посилання, що не вказує, воно зберігається безпосередньо у стеку або в межах іншого об'єкта, ala будує в C #.


5

Як і для всіх типів цінності, структури завжди йдуть туди, де вони були оголошені .

Дивіться це питання тут, щоб отримати докладнішу інформацію про те, коли використовувати структури. І це питання тут для отримання додаткової інформації про структури.

Редагувати: я помилково відповів, що ЗАВЖДИ вони йдуть у групі. Це неправильно .


"структури завжди йдуть туди, де вони були оголошені", це трохи вводить в оману заплутаність. Структурне поле в класі завжди поміщається в "динамічну пам'ять, коли будується екземпляр типу" - Джефф Ріхтер. Це може бути опосередковано на купі, але зовсім не таке, як нормальний тип відліку.
Еш

Ні, я думаю, що це абсолютно правильно - хоча це не те саме, що і референсний тип. Значення змінної живе там, де вона оголошена. Значення змінної типу посилання - це посилання, а не фактичні дані, це все.
Джон Скіт

Підсумовуючи це, кожен раз, коли ви створюєте (оголошуєте) тип значення в будь-якому місці методу, він завжди створюється на стеку.
Еш

2
Джон, ти сумуєш за моєю точкою. Причина, яку вперше задали це питання, полягає в тому, що багатьом розробникам не зрозуміло (включаючи мене, поки я не прочитав CLR Via C #), куди виділяється структура, якщо для її створення використовується новий оператор. Сказати, що "структури завжди йдуть там, де їх оголосили", не є однозначною відповіддю.
Еш

1
@Ash: Якщо у мене буде час, я спробую написати відповідь, коли прийду на роботу. Це занадто велика тема, щоб спробувати висвітлити у поїзді :)
Джон Скіт

4

Я, мабуть, щось тут пропускаю, але чому ми дбаємо про виділення?

Типи значень передаються за значенням;) і, таким чином, не можуть бути мутовані в іншому масштабі, ніж там, де вони визначені. Щоб мати змогу вимкнути значення, потрібно додати ключове слово [ref].

Типи посилань передаються шляхом посилання та можуть бути вимкнено.

Звичайно, найпопулярнішими є рядки незмінних посилальних типів.

Макет / ініціалізація масиву: типи значень -> нульова пам'ять [ім'я, zip] [ім'я, zip] Типи довідок -> нульова пам'ять -> null [ref] [ref]


3
Типи посилань не передаються посиланнями - посилання передаються за значенням. Це зовсім інакше.
Джон Скіт

2

classАбо structдекларація , як план , який використовується для створення екземплярів або об'єктів під час виконання. Якщо ви визначаєте особу classабо structназиваєте її особою, особа - це назва типу. Якщо ви оголошуєте та ініціалізуєте змінну p типу Person, p, як кажуть, є об'єктом або екземпляром Person. Можна створити кілька примірників одного типу Person, і кожен екземпляр може мати різні значення у своїх propertiesта fields.

А class- еталонний тип. Коли створюється об'єкт class, змінна, якій призначений об'єкт, містить лише посилання на цю пам'ять. Коли посилання на об'єкт присвоюється новій змінній, нова змінна відноситься до вихідного об'єкта. Зміни, внесені через одну змінну, відображаються в іншій змінній, оскільки вони обоє посилаються на одні і ті ж дані.

A struct- тип значення. Коли а structстворюється, змінна, якій structприсвоєно, містить фактичні дані структури. Коли structприсвоєно новій змінній, вона копіюється. Нова змінна та оригінальна змінна тому містять дві окремі копії одних і тих же даних. Зміни, внесені до однієї копії, не впливають на іншу.

Взагалі, classesвикористовуються для моделювання більш складної поведінки або даних, які призначені для зміни після створення classоб'єкта. Structsнайкраще підходять для невеликих структур даних, які містять в основному дані, які не призначені для зміни після structстворення.

для більш...


1

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


1

Конструкції виділяються в стек. Ось корисне пояснення:

Конструкції

Крім того, класи, коли інстанціюється в .NET, виділяють пам’ять на купі або в резервованому просторі пам'яті .NET. Тоді як конструкції приносять більшу ефективність при інстанціюванні завдяки розподілу на стеку. Крім того, слід зазначити, що передача параметрів у структурах робиться це за значенням.


5
Це не охоплює випадок, коли структура є частиною класу - в цей момент вона живе на купі, а решта даних об'єкта.
Джон Скіт

1
Так, але він фактично фокусується на і відповідає на запитання, яке задають. Голосували.
Еш

... і досі залишаються невірними та оманливими. Вибачте, але коротких відповідей на це питання немає - Єффрі є єдиною повною відповіддю.
Марк Гравелл
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.