Czy użycie „new” w strukturze przydziela go na stos lub stos?

290

Podczas tworzenia wystąpienia klasy z newoperatorem pamięć jest przydzielana na stercie. Kiedy tworzysz wystąpienie struktury z newoperatorem, gdzie przydzielana jest pamięć, na stercie lub na stosie?

kedar kamthe
źródło

Odpowiedzi:

305

Dobra, zobaczmy, czy mogę to wyjaśnić.

Po pierwsze, Ash ma rację: pytanie nie dotyczy tego, gdzie przydzielane są zmienne typu wartości . To inne pytanie - na które odpowiedź nie polega tylko na „stosie”. To jest bardziej skomplikowane (i jeszcze bardziej skomplikowane przez C # 2). Mam artykuł na ten temat i rozwinę go na żądanie, ale porozmawiajmy tylko z newoperatorem.

Po drugie, wszystko to naprawdę zależy od poziomu, o którym mówisz. Patrzę na to, co kompilator robi z kodem źródłowym, pod względem tworzonej IL. Jest więcej niż możliwe, że kompilator JIT zrobi sprytne rzeczy, jeśli chodzi o zoptymalizowanie całkiem sporo „logicznej” alokacji.

Po trzecie, ignoruję generyczne, głównie dlatego, że tak naprawdę nie znam odpowiedzi, a częściowo dlatego, że skomplikowałoby to wszystko zbytnio.

Wreszcie, wszystko to odbywa się w ramach obecnego wdrożenia. Specyfikacja C # nie precyzuje wiele z tego - to faktycznie szczegół implementacji. Są tacy, którzy uważają, że twórcy kodu zarządzanego naprawdę nie powinni się przejmować. Nie jestem pewien, czy posunę się tak daleko, ale warto wyobrazić sobie świat, w którym w rzeczywistości wszystkie lokalne zmienne żyją na stercie - co nadal byłoby zgodne ze specyfikacją.


Istnieją dwie różne sytuacje z newoperatorem dla typów wartości: możesz wywołać konstruktora bez parametrów (np. new Guid()) Lub konstruktora parametrycznego (np new Guid(someString).). Generują one znacząco różne IL. Aby zrozumieć, dlaczego, musisz porównać specyfikacje C # i CLI: zgodnie z C # wszystkie typy wartości mają konstruktor bez parametrów. Zgodnie ze specyfikacją CLI żadne typy wartości nie mają konstruktorów bez parametrów. (Pobierz konstruktory typu wartości z odbiciem jakiś czas - nie znajdziesz parametru bez parametrów).

C # ma sens traktować „inicjowanie wartości zerami” jako konstruktor, ponieważ utrzymuje spójność języka - możesz myśleć o tym, new(...)że zawsze wywołujesz konstruktor. CLI ma sens, aby myśleć o tym inaczej, ponieważ nie ma prawdziwego kodu do wywołania - a na pewno nie ma kodu specyficznego dla typu.

Ma również znaczenie to, co zrobisz z wartością po jej zainicjowaniu. IL używany do

Guid localVariable = new Guid(someString);

różni się od IL zastosowanej do:

myInstanceOrStaticVariable = new Guid(someString);

Ponadto, jeśli wartość jest używana jako wartość pośrednia, np. Jako argument wywołania metody, sytuacja znów się nieco różni. Aby pokazać wszystkie te różnice, oto krótki program testowy. Nie pokazuje różnicy między zmiennymi statycznymi a zmiennymi instancji: IL różni się między stfldi stsfld, ale to wszystko.

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());
    }
}

Oto IL dla klasy, z wyłączeniem nieistotnych bitów (takich jak 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
}

Jak widać, istnieje wiele różnych instrukcji służących do wywoływania konstruktora:

  • newobj: Przydziela wartość na stosie, wywołuje sparametryzowany konstruktor. Używany do wartości pośrednich, np. Do przypisania do pola lub jako argument metody.
  • call instance: Używa już przydzielonej lokalizacji pamięci (na stosie lub nie). Jest to użyte w powyższym kodzie do przypisania do zmiennej lokalnej. Jeśli ta sama zmienna lokalna zostanie kilkakrotnie przypisana do wartości za pomocą kilku newwywołań, po prostu inicjuje dane ponad starą wartością - za każdym razem nie przydziela więcej miejsca na stosie.
  • initobj: Używa już przydzielonej lokalizacji pamięci i po prostu czyści dane. Jest to wykorzystywane do wszystkich naszych bezparametrowych wywołań konstruktora, w tym tych, które przypisują zmienną lokalną. W wywołaniu metody efektywnie wprowadza się pośrednią zmienną lokalną, a jej wartość usuwa się initobj.

Mam nadzieję, że to pokazuje, jak skomplikowany jest ten temat, jednocześnie rzucając na niego nieco światła. W niektórych koncepcyjnych sensach każde wezwanie do newprzydzielenia miejsca na stosie - ale jak widzieliśmy, tak naprawdę nie dzieje się nawet na poziomie IL. Chciałbym podkreślić jeden konkretny przypadek. Weź tę metodę:

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

To „logicznie” ma 4 przydziały stosu - po jednym dla zmiennej i po jednym dla każdego z trzech newwywołań - ale w rzeczywistości (dla tego konkretnego kodu) stos jest przydzielany tylko raz, a następnie ta sama lokalizacja pamięci jest ponownie wykorzystywana.

EDYCJA: Żeby było jasne, jest to prawdą tylko w niektórych przypadkach ... w szczególności wartość guidnie będzie widoczna, jeśli Guidkonstruktor zgłosi wyjątek, dlatego kompilator C # jest w stanie ponownie użyć tego samego miejsca na stosie. Zobacz post na blogu Erica Lipperta o konstrukcji typu wartości, aby uzyskać więcej szczegółów i przypadek, w którym nie ma zastosowania.

Nauczyłem się wiele, pisząc tę ​​odpowiedź - poproś o wyjaśnienie, jeśli coś jest niejasne!

Jon Skeet
źródło
1
Jon, przykładowy kod HowManyStackAllocations jest dobry. Ale czy możesz to zmienić, aby użyć Struct zamiast Guid, lub dodać nowy przykład Struct? Myślę, że to bezpośrednio dotyczyłoby pierwotnego pytania @ kedar.
Ash
9
Guid jest już strukturą. Zobacz msdn.microsoft.com/en-us/library/system.guid.aspx Nie wybrałbym typu referencyjnego dla tego pytania :)
Jon Skeet
1
Co się stanie, gdy je List<Guid>dodasz i dodasz 3? To byłyby 3 przydziały (ta sama IL)? Ale są trzymane gdzieś magicznie
Arec Barrwin
1
@Ani: Brakuje Ci faktu, że przykład Erica zawiera blok try / catch - więc jeśli wyjątek zostanie zgłoszony podczas konstruktora struct, musisz zobaczyć wartość przed konstruktorem. W moim przykładzie nie ma takiej sytuacji - jeśli konstruktor zawiedzie z wyjątkiem, nie ma znaczenia, czy wartość parametru guidzostała tylko częściowo nadpisana, ponieważ i tak nie będzie widoczna.
Jon Skeet,
2
@Ani: W rzeczywistości Eric nazywa to u dołu swojego postu: „A co z punktem Wesnera? Tak, w rzeczywistości, jeśli deklarowana jest zmienna lokalna przydzielona do stosu (a nie pole w zamknięciu), która jest zadeklarowana na tym samym poziomie „próbowania” zagnieżdżenia, co wywołanie konstruktora, nie przechodzimy przez ten sztywny tryb tworzenia nowego tymczasowego, inicjowania tymczasowego i kopiowania go do lokalnego. W tym konkretnym (i powszechnym) przypadku możemy zoptymalizować utworzenie tymczasowej i kopii, ponieważ program C # nie jest w stanie zauważyć różnicy! "
Jon Skeet,
40

Pamięć zawierająca pola struktury może być przydzielona albo na stosie, albo na stosie, w zależności od okoliczności. Jeśli zmienna typu struct jest zmienną lokalną lub parametrem, który nie jest przechwytywany przez anonimową klasę delegata lub iteratora, to zostanie ona przydzielona na stosie. Jeśli zmienna jest częścią jakiejś klasy, to zostanie przydzielona w klasie na stercie.

Jeśli struktura jest przydzielona na stercie, wywołanie nowego operatora nie jest w rzeczywistości konieczne do przydzielenia pamięci. Jedynym celem byłoby ustawienie wartości pola zgodnie z tym, co jest w konstruktorze. Jeśli konstruktor nie zostanie wywołany, wszystkie pola otrzymają wartości domyślne (0 lub null).

Podobnie dla struktur przydzielonych na stosie, z tym wyjątkiem, że C # wymaga, aby wszystkie zmienne lokalne były ustawione na pewną wartość przed ich użyciem, więc musisz wywołać konstruktor niestandardowy lub domyślny (konstruktor, który nie przyjmuje parametrów, jest zawsze dostępny dla struktury).

Jeffrey L. Whitledge
źródło
13

Mówiąc krótko, new jest mylącą nazwą dla struktur, wywoływanie new po prostu wywołuje konstruktor. Jedyną lokalizacją pamięci dla struktury jest lokalizacja, w której jest zdefiniowana.

Jeśli jest to zmienna składowa, jest ona przechowywana bezpośrednio w dowolnej definicji, jeśli jest zmienną lokalną lub parametrem, jest przechowywana na stosie.

Porównaj to z klasami, które mają odniesienie wszędzie tam, gdzie struktura byłaby przechowywana w całości, podczas gdy odniesienie wskazuje gdzieś na stercie. (Członek wewnątrz, lokalny / parametr na stosie)

Pomocne może być spojrzenie na C ++, gdzie nie ma rzeczywistego rozróżnienia między klasą / strukturą. (W języku są podobne nazwy, ale odnoszą się one tylko do domyślnej dostępności rzeczy). Kiedy wywołujesz new, dostajesz wskaźnik do lokalizacji stosu, a jeśli masz odwołanie inne niż wskaźnik, jest ono przechowywane bezpośrednio na stosie lub w drugim obiekcie ala structs w C #.

Guvante
źródło
5

Podobnie jak w przypadku wszystkich typów wartości, struktury zawsze idą tam, gdzie zostały zadeklarowane .

Zobacz to pytanie tutaj, aby uzyskać więcej informacji o tym, kiedy używać struktur. I to pytanie tutaj aby uzyskać więcej informacji na temat struktur.

Edycja: błędnie odpowiedziałem, że ZAWSZE wchodzą na stos. To jest błędne .

Esteban Araya
źródło
„struktury zawsze idą tam, gdzie zostały zadeklarowane”, jest to nieco mylące. Pole struct w klasie jest zawsze umieszczane w „pamięci dynamicznej, gdy budowana jest instancja tego typu” - Jeff Richter. Może to być pośrednio na stercie, ale wcale nie jest takie samo jak normalny typ odwołania.
Ash
Nie, myślę, że jest dokładnie w porządku - nawet jeśli nie jest taki sam jak typ odniesienia. Wartość zmiennej żyje tam, gdzie jest zadeklarowana. Wartością zmiennej typu odwołania jest odwołanie, zamiast rzeczywistych danych, to wszystko.
Jon Skeet,
Podsumowując, za każdym razem, gdy tworzysz (deklarujesz) typ wartości w dowolnym miejscu metody, jest on zawsze tworzony na stosie.
Ash
2
Jon, tęsknisz za moim celem. Powodem, dla którego to pytanie zostało zadane po raz pierwszy, jest to, że dla wielu programistów (mnie włącznie, dopóki nie przeczytam CLR Via C #) nie jest jasne, gdzie jest przydzielona struktura, jeśli użyjesz nowego operatora do jej utworzenia. Powiedzenie „struktury zawsze idą tam, gdzie zostały zadeklarowane”, nie jest jednoznaczną odpowiedzią.
Ash
1
@ Ash: Jeśli będę miał czas, postaram się napisać odpowiedź, kiedy dojdę do pracy. Jest to jednak zbyt duży temat, aby spróbować porozmawiać w pociągu :)
Jon Skeet
4

Prawdopodobnie coś tu brakuje, ale dlaczego zależy nam na alokacji?

Typy wartości są przekazywane przez wartość;) i dlatego nie można ich mutować w innym zakresie niż tam, gdzie są zdefiniowane. Aby móc mutować wartość, musisz dodać słowo kluczowe [ref].

Typy referencyjne są przekazywane przez referencję i mogą być mutowane.

Oczywiście są najbardziej niezmienne ciągi typów referencyjnych.

Układ / inicjalizacja tablicy: Typy wartości -> zerowa pamięć [nazwa, zip] [nazwa, zip] Typy referencyjne -> zerowa pamięć -> null [ref] [ref]

użytkownik18579
źródło
3
Typy referencji nie są przekazywane przez referencje - referencje są przekazywane przez wartość. To bardzo różne.
Jon Skeet
2

classLub structdeklaracja jest jak plan, który jest używany do utworzenia instancji lub przedmiotów w czasie wykonywania. Jeśli zdefiniujesz classlub o structnazwie Osoba, Osoba jest nazwą typu. Jeśli zadeklarujesz i zainicjujesz zmienną p typu Person, p zostanie uznane za obiekt lub instancję Person. Można utworzyć wiele wystąpień tego samego typu Osoba, a każde wystąpienie może mieć różne wartości w swoim propertiesi fields.

A classjest typem odniesienia. Gdy obiektclass zmiennej zmienna, do której obiekt jest przypisany, zawiera tylko odwołanie do tej pamięci. Gdy odwołanie do obiektu jest przypisane do nowej zmiennej, nowa zmienna odnosi się do oryginalnego obiektu. Zmiany dokonane za pomocą jednej zmiennej są odzwierciedlone w drugiej zmiennej, ponieważ obie odnoszą się do tych samych danych.

A structjest typem wartości. Kiedy structtworzony jest a , zmienna, do której structjest przypisany, przechowuje rzeczywiste dane struktury. Po structprzypisaniu do nowej zmiennej jest ona kopiowana. Nowa zmienna i pierwotna zmienna zawierają zatem dwie osobne kopie tych samych danych. Zmiany wprowadzone w jednej kopii nie wpływają na drugą kopię.

Zasadniczo classessłużą do modelowania bardziej złożonych zachowań lub danych, które mają zostać zmodyfikowane po utworzeniu classobiektu. Structsnajlepiej nadają się do małych struktur danych, które zawierają przede wszystkim dane, które nie są modyfikowane po structutworzeniu.

po więcej ...

Sujit
źródło
1

Prawie struktury, które są uważane za typy wartości, są przydzielane na stosie, podczas gdy obiekty są przydzielane na stercie, podczas gdy odniesienie do obiektu (wskaźnik) jest przydzielane na stosie.

bashmohandes
źródło
1

Struktury są przydzielane do stosu. Oto pomocne wyjaśnienie:

Struktury

Ponadto klasy tworzone w instancji w .NET przydzielają pamięć na stercie lub w zarezerwowanej przestrzeni pamięci .NET. Natomiast struktury dają większą wydajność, gdy są tworzone instancje dzięki alokacji na stosie. Ponadto należy zauważyć, że przekazywanie parametrów w strukturach odbywa się według wartości.

DaveK
źródło
5
Nie dotyczy to przypadku, gdy struct jest częścią klasy - w którym momencie żyje on na stercie wraz z resztą danych obiektu.
Jon Skeet
1
Tak, ale w rzeczywistości koncentruje się i odpowiada na zadane pytanie. Zagłosowano.
Ash
... wciąż będąc niepoprawnym i wprowadzającym w błąd. Przepraszamy, ale nie ma krótkich odpowiedzi na to pytanie - jedyną pełną odpowiedzią jest Jeffrey's.
Marc Gravell