Apakah menggunakan "baru" pada struct mengalokasikannya di heap atau stack?

290

Saat Anda membuat instance kelas dengan newoperator, memori akan dialokasikan pada heap. Ketika Anda membuat instance dari struct dengan newoperator di mana memori dialokasikan, di tumpukan atau di tumpukan?

kedar kamthe
sumber

Jawaban:

305

Oke, mari kita lihat apakah saya bisa memperjelas ini.

Pertama, Ash benar: pertanyaannya bukan tentang di mana variabel tipe nilai dialokasikan. Itu pertanyaan yang berbeda - dan yang jawabannya tidak hanya "di tumpukan". Ini lebih rumit dari itu (dan dibuat lebih rumit oleh C # 2). Saya memiliki artikel tentang topik ini dan akan diperluas jika diminta, tetapi mari kita berurusan dengan newoperator saja.

Kedua, semua ini sangat tergantung pada level apa yang Anda bicarakan. Saya melihat apa yang dilakukan kompiler dengan kode sumber, dalam hal IL yang dibuatnya. Lebih dari kemungkinan bahwa kompiler JIT akan melakukan hal-hal pintar dalam hal mengoptimalkan alokasi "logis" yang cukup banyak.

Ketiga, saya mengabaikan obat generik, kebanyakan karena saya sebenarnya tidak tahu jawabannya, dan sebagian lagi karena terlalu rumit.

Akhirnya, semua ini hanya dengan implementasi saat ini. C # spec tidak menentukan banyak tentang hal ini - ini adalah detail implementasi yang efektif. Ada orang yang percaya bahwa pengembang kode terkelola benar-benar tidak peduli. Saya tidak yakin saya akan melangkah sejauh itu, tapi ada baiknya membayangkan sebuah dunia di mana sebenarnya semua variabel lokal tinggal di heap - yang masih akan sesuai dengan spesifikasi.


Ada dua situasi berbeda dengan newoperator pada tipe nilai: Anda dapat memanggil konstruktor tanpa parameter (misalnya new Guid()) atau konstruktor penuh parameter (misalnya new Guid(someString)). Ini menghasilkan IL yang sangat berbeda. Untuk memahami alasannya, Anda perlu membandingkan spesifikasi C # dan CLI: menurut C #, semua tipe nilai memiliki konstruktor tanpa parameter. Menurut spesifikasi CLI, tidak ada tipe nilai yang memiliki konstruktor tanpa parameter. (Ambil konstruktor dari tipe nilai dengan refleksi beberapa waktu - Anda tidak akan menemukan yang tanpa parameter.)

Masuk akal bagi C # untuk memperlakukan "inisialisasi nilai dengan nol" sebagai konstruktor, karena itu membuat bahasa tetap konsisten - Anda dapat menganggapnya new(...)sebagai selalu memanggil konstruktor. Masuk akal bagi CLI untuk memikirkannya secara berbeda, karena tidak ada kode nyata untuk dipanggil - dan tentu saja tidak ada kode jenis khusus.

Ini juga membuat perbedaan apa yang akan Anda lakukan dengan nilai setelah Anda menginisialisasi. IL digunakan untuk

Guid localVariable = new Guid(someString);

berbeda dengan IL yang digunakan untuk:

myInstanceOrStaticVariable = new Guid(someString);

Selain itu, jika nilai tersebut digunakan sebagai nilai perantara, misalnya argumen untuk pemanggilan metode, hal-hal sedikit berbeda lagi. Untuk menunjukkan semua perbedaan ini, inilah program tes singkat. Itu tidak menunjukkan perbedaan antara variabel statis dan variabel instan: IL akan berbeda antara stflddan stsfld, tapi itu saja.

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

Inilah IL untuk kelas, tidak termasuk bit yang tidak relevan (seperti 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
}

Seperti yang Anda lihat, ada banyak instruksi berbeda yang digunakan untuk memanggil konstruktor:

  • newobj: Mengalokasikan nilai pada stack, memanggil konstruktor parameterised. Digunakan untuk nilai menengah, misalnya untuk penugasan ke bidang atau digunakan sebagai argumen metode.
  • call instance: Menggunakan lokasi penyimpanan yang sudah dialokasikan (baik di tumpukan atau tidak). Ini digunakan dalam kode di atas untuk menetapkan ke variabel lokal. Jika variabel lokal yang sama diberikan nilai beberapa kali menggunakan beberapa newpanggilan, itu hanya menginisialisasi data di atas nilai lama - itu tidak mengalokasikan lebih banyak ruang stack setiap kali.
  • initobj: Menggunakan lokasi penyimpanan yang sudah dialokasikan dan hanya menghapus data. Ini digunakan untuk semua panggilan konstruktor tanpa parameter kami, termasuk yang menetapkan untuk variabel lokal. Untuk pemanggilan metode, variabel lokal perantara diperkenalkan secara efektif, dan nilainya dihapus oleh initobj.

Saya harap ini menunjukkan betapa rumitnya topik itu, sambil menyinari sedikit cahaya pada saat yang bersamaan. Dalam beberapa pengertian konseptual, setiap panggilan untuk newmengalokasikan ruang pada stack - tetapi seperti yang telah kita lihat, itu bukanlah yang sebenarnya terjadi bahkan pada level IL. Saya ingin menyoroti satu kasus khusus. Ambil metode ini:

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

Bahwa "secara logis" memiliki 4 alokasi stack - satu untuk variabel, dan satu untuk masing-masing dari tiga newpanggilan - tetapi pada kenyataannya (untuk kode spesifik itu) stack hanya dialokasikan satu kali, dan kemudian lokasi penyimpanan yang sama digunakan kembali.

EDIT: Hanya untuk memperjelas, ini hanya benar dalam beberapa kasus ... khususnya, nilai guidtidak akan terlihat jika Guidkonstruktor melempar pengecualian, itulah sebabnya kompiler C # dapat menggunakan kembali slot stack yang sama. Lihat posting blog Eric Lippert tentang konstruksi tipe nilai untuk detail lebih lanjut dan kasus di mana itu tidak berlaku.

Saya telah belajar banyak dalam menulis jawaban ini - silakan minta klarifikasi jika ada yang tidak jelas!

Jon Skeet
sumber
1
Jon, kode contoh HowManyStackAllocations bagus. Tetapi dapatkah Anda mengubahnya untuk menggunakan Struct sebagai ganti Guid, atau menambahkan contoh Struct baru. Saya pikir itu akan langsung menjawab pertanyaan awal @ kedar.
Ash
9
Guid sudah menjadi struct. Lihat msdn.microsoft.com/en-us/library/system.guid.aspx Saya tidak akan memilih jenis referensi untuk pertanyaan ini :)
Jon Skeet
1
Apa yang terjadi ketika Anda memiliki List<Guid>dan menambahkan 3 itu? Itu akan menjadi 3 alokasi (IL yang sama)? Tapi mereka disimpan di suatu tempat yang ajaib
Arec Barrwin
1
@ Ai: Anda melewatkan fakta bahwa contoh Eric memiliki blok coba / tangkap - jadi jika pengecualian dilemparkan selama konstruktor struct, Anda harus dapat melihat nilai sebelum konstruktor. Contoh saya tidak memiliki situasi seperti itu - jika konstruktor gagal dengan pengecualian, tidak masalah jika nilai guidhanya setengah ditimpa, karena tidak akan terlihat.
Jon Skeet
2
@Ani: Sebenarnya, Eric menyebut ini di dekat bagian bawah jabatannya: "Sekarang, bagaimana dengan poin Wesner? Ya, sebenarnya jika itu adalah variabel lokal yang dialokasikan stack (dan bukan bidang dalam penutupan) yang dideklarasikan pada tingkat yang sama "mencoba" bersarang sebagai panggilan konstruktor maka kita tidak melalui kesulitan ini membuat sementara baru, menginisialisasi sementara, dan menyalinnya ke lokal. Dalam kasus tertentu (dan umum) kita dapat mengoptimalkan pergi pembuatan sementara dan salinan karena tidak mungkin bagi program C # untuk mengamati perbedaannya! "
Jon Skeet
40

Memori yang berisi bidang struct dapat dialokasikan pada tumpukan atau tumpukan tergantung pada keadaan. Jika variabel tipe-struktural adalah variabel lokal atau parameter yang tidak ditangkap oleh beberapa delegasi anonim atau kelas iterator, maka itu akan dialokasikan pada stack. Jika variabel adalah bagian dari beberapa kelas, maka itu akan dialokasikan di dalam kelas di heap.

Jika struct dialokasikan pada heap, maka memanggil operator baru sebenarnya tidak perlu mengalokasikan memori. Satu-satunya tujuan adalah untuk menetapkan nilai bidang sesuai dengan apa pun yang ada di konstruktor. Jika konstruktor tidak dipanggil, maka semua bidang akan mendapatkan nilai defaultnya (0 atau nol).

Demikian pula untuk struct yang dialokasikan pada stack, kecuali bahwa C # mengharuskan semua variabel lokal diatur ke beberapa nilai sebelum mereka digunakan, jadi Anda harus memanggil konstruktor kustom atau konstruktor default (konstruktor yang tidak mengambil parameter selalu tersedia untuk struct).

Jeffrey L Whitledge
sumber
13

Singkatnya, baru adalah keliru untuk struct, memanggil baru hanya memanggil konstruktor. Satu-satunya lokasi penyimpanan untuk struct adalah lokasi yang ditentukan.

Jika itu adalah variabel anggota, ia disimpan secara langsung dalam apa pun yang didefinisikan, jika itu adalah variabel lokal atau parameter itu disimpan di tumpukan.

Bandingkan ini dengan kelas, yang memiliki referensi di mana pun struct akan disimpan secara keseluruhan, sementara referensi menunjuk ke suatu tempat di heap. (Anggota dalam, lokal / parameter pada tumpukan)

Mungkin membantu untuk melihat sedikit ke dalam C ++, di mana tidak ada perbedaan nyata antara kelas / struct. (Ada nama-nama yang mirip dalam bahasa, tetapi mereka hanya merujuk pada aksesibilitas default hal-hal) Ketika Anda menelepon baru Anda mendapatkan pointer ke lokasi tumpukan, sementara jika Anda memiliki referensi non-pointer itu disimpan langsung di tumpukan atau dalam objek lain, ala struct di C #.

Guvante
sumber
5

Seperti dengan semua jenis nilai, struct selalu pergi ke tempat yang dideklarasikan .

Lihat pertanyaan ini di sini untuk detail lebih lanjut tentang kapan harus menggunakan struct. Dan pertanyaan ini di sini untuk info lebih lanjut tentang struct.

Sunting: Saya telah salah menjawab bahwa mereka SELALU masuk ke tumpukan. Ini salah .

Esteban Araya
sumber
"struct selalu pergi ke tempat mereka dinyatakan", ini agak menyesatkan membingungkan. Bidang struct di kelas selalu ditempatkan ke "memori dinamis ketika instance dari tipe dibangun" - Jeff Richter. Ini mungkin secara tidak langsung di heap, tetapi tidak sama dengan tipe referensi normal sama sekali.
Ash
Tidak, saya pikir itu persis benar - meskipun itu tidak sama dengan tipe referensi. Nilai dari suatu variabel tinggal di mana ia dideklarasikan. Nilai variabel tipe referensi adalah referensi, bukan data aktual, itu saja.
Jon Skeet
Singkatnya, setiap kali Anda membuat (menyatakan) tipe nilai di mana saja dalam metode itu selalu dibuat di stack.
Ash
2
Jon, kamu kehilangan maksud saya. Alasan pertanyaan ini pertama kali ditanyakan adalah karena tidak jelas bagi banyak pengembang (termasuk saya sampai saya membaca CLR Via C #) di mana struct dialokasikan jika Anda menggunakan operator baru untuk membuatnya. Mengatakan "struct selalu pergi ke tempat yang dideklarasikan" tidak bukanlah jawaban yang jelas.
Ash
1
@ Ash: Jika saya punya waktu, saya akan mencoba menulis jawaban ketika saya mulai bekerja. Ini topik yang terlalu besar untuk dicoba di kereta :)
Jon Skeet
4

Saya mungkin melewatkan sesuatu di sini, tetapi mengapa kita peduli tentang alokasi?

Jenis nilai dilewatkan oleh nilai;) dan karenanya tidak dapat dimutasi pada ruang lingkup yang berbeda dari tempat mereka didefinisikan. Untuk dapat mengubah nilai Anda harus menambahkan kata kunci [ref].

Jenis referensi dilewatkan oleh referensi dan dapat dimutasi.

Tentu saja ada string tipe referensi yang tidak berubah menjadi yang paling populer.

Tata letak / inisialisasi array: Tipe nilai -> zero memory [nama, zip] [nama, zip] Jenis referensi -> zero memory -> null [ref] [ref]

pengguna18579
sumber
3
Jenis referensi tidak lulus oleh referensi - referensi diberikan oleh nilai. Itu sangat berbeda.
Jon Skeet
2

Pernyataan classatau structdeklarasi seperti cetak biru yang digunakan untuk membuat instance atau objek pada saat run time. Jika Anda mendefinisikan classatau structdisebut Person, Person adalah nama dari tipe tersebut. Jika Anda mendeklarasikan dan menginisialisasi p variabel tipe Person, p dikatakan sebagai objek atau instance Person. Beberapa instance dari tipe Person yang sama dapat dibuat, dan setiap instance dapat memiliki nilai yang berbeda di dalamnya propertiesdan fields.

A classadalah tipe referensi. Ketika sebuah objek classdibuat, variabel yang objek ditugaskan hanya memiliki referensi ke memori itu. Ketika referensi objek ditugaskan ke variabel baru, variabel baru merujuk ke objek asli. Perubahan yang dilakukan melalui satu variabel tercermin dalam variabel lain karena keduanya merujuk pada data yang sama.

A structadalah tipe nilai. Ketika a structdibuat, variabel yang structditugaskan menyimpan data aktual struct. Ketika structditugaskan ke variabel baru, itu akan disalin. Variabel baru dan variabel asli karenanya mengandung dua salinan terpisah dari data yang sama. Perubahan yang dilakukan pada satu salinan tidak memengaruhi salinan lainnya.

Secara umum, classesdigunakan untuk memodelkan perilaku yang lebih kompleks, atau data yang dimaksudkan untuk dimodifikasi setelah classobjek dibuat. Structspaling cocok untuk struktur data kecil yang terutama berisi data yang tidak dimaksudkan untuk dimodifikasi setelah structdibuat.

untuk lebih...

Sujit
sumber
1

Cukup banyak struct yang dianggap tipe Value, dialokasikan pada stack, sementara objek dialokasikan pada heap, sedangkan referensi objek (pointer) akan dialokasikan pada stack.

bashmohandes
sumber
1

Structs dialokasikan ke stack. Berikut ini penjelasan yang bermanfaat:

Structs

Selain itu, kelas ketika dipakai dalam. NET mengalokasikan memori pada heap atau ruang memori yang disediakan .NET. Sedangkan struct menghasilkan lebih banyak efisiensi ketika instantiated karena alokasi pada stack. Lebih lanjut, harus dicatat bahwa melewati parameter dalam struct dilakukan dengan nilai.

DaveK
sumber
5
Ini tidak mencakup kasus ketika struct adalah bagian dari kelas - di mana titik itu hidup di heap, dengan sisa data objek.
Jon Skeet
1
Ya tetapi sebenarnya berfokus pada dan menjawab pertanyaan yang diajukan. Terpilih.
Ash
... sementara masih salah dan menyesatkan. Maaf, tetapi tidak ada jawaban singkat untuk pertanyaan ini - Jeffrey adalah satu-satunya jawaban yang lengkap.
Marc Gravell