Struktur versus kelas

94

Saya akan membuat 100.000 objek dalam kode. Mereka kecil, hanya dengan 2 atau 3 properti. Saya akan menempatkannya dalam daftar umum dan jika sudah, saya akan mengulanginya dan memeriksa nilai adan mungkin memperbarui nilai b.

Apakah lebih cepat / lebih baik untuk membuat objek ini sebagai kelas atau sebagai struct?

EDIT

Sebuah. Properti adalah tipe nilai (kecuali string yang menurut saya?)

b. Mereka mungkin (kami belum yakin) memiliki metode validasi

EDIT 2

Saya bertanya-tanya: apakah objek di heap dan tumpukan diproses secara sama oleh pengumpul sampah, atau apakah cara kerjanya berbeda?

Michel
sumber
2
Apakah mereka hanya akan memiliki lapangan publik, atau mereka juga akan memiliki metode? Apakah tipe tipe primitif, seperti integer? Apakah mereka akan dimuat dalam sebuah array, atau dalam sesuatu seperti List <T>?
JeffFerguson
14
Daftar struct yang bisa berubah? Hati-hati dengan velociraptor.
Anthony Pegram
1
@Anthony: Saya khawatir saya kehilangan lelucon velociraptor: -s
Michel
5
Lelucon velociraptor berasal dari XKCD. Tetapi ketika Anda melempar sekitar kesalahpahaman 'tipe nilai dialokasikan pada tumpukan' / detail implementasi (hapus jika berlaku) maka Eric Lippert yang perlu Anda waspadai ...
Greg Beech
4
velociraptor: imgs.xkcd.com/comics/goto.png
WernerCD

Jawaban:

137

Apakah lebih cepat membuat objek ini sebagai kelas atau sebagai struct?

Anda adalah satu-satunya orang yang dapat menentukan jawaban atas pertanyaan itu. Cobalah kedua cara tersebut, ukur metrik kinerja yang bermakna, berfokus pada pengguna, dan relevan, lalu Anda akan mengetahui apakah perubahan tersebut memiliki efek yang berarti pada pengguna nyata dalam skenario yang relevan.

Struktur mengonsumsi lebih sedikit memori heap (karena lebih kecil dan lebih mudah dipadatkan, bukan karena "ada di tumpukan"). Tetapi mereka membutuhkan waktu lebih lama untuk menyalin daripada salinan referensi. Saya tidak tahu apa metrik kinerja Anda untuk penggunaan atau kecepatan memori; ada pengorbanan di sini dan Anda adalah orang yang tahu apa itu.

Apakah lebih baik membuat objek ini sebagai kelas atau sebagai struct?

Mungkin kelas, mungkin struct. Sebagai aturan praktis: Jika objeknya adalah:
1. Kecil
2. Secara logis, nilai yang tidak dapat diubah
3. Ada banyak nilai .
Maka saya akan mempertimbangkan untuk menjadikannya sebuah struct. Jika tidak, saya akan tetap menggunakan tipe referensi.

Jika Anda perlu mengubah beberapa bidang dari sebuah struct, biasanya lebih baik membuat konstruktor yang mengembalikan struct baru dengan bidang yang disetel dengan benar. Itu mungkin sedikit lebih lambat (ukurlah!) Tetapi secara logis lebih mudah untuk bernalar.

Apakah objek di heap dan tumpukan diproses secara sama oleh pengumpul sampah?

Tidak , keduanya tidak sama karena objek di tumpukan adalah akar dari koleksi . Pengumpul sampah tidak perlu bertanya "apakah benda yang ada di tumpukan ini hidup?" karena jawaban atas pertanyaan itu selalu "Ya, ada di tumpukan". (Sekarang, Anda tidak dapat mengandalkan itu untuk menjaga objek tetap hidup karena tumpukan adalah detail implementasi. Jitter diizinkan untuk memperkenalkan pengoptimalan yang, katakanlah, mendaftarkan apa yang biasanya menjadi nilai tumpukan, dan kemudian tidak pernah ada di tumpukan jadi GC tidak tahu bahwa ia masih hidup. Objek yang didaftarkan dapat dikumpulkan turunannya secara agresif, segera setelah register yang memegangnya tidak akan dibaca lagi.)

Tapi pengumpul sampah tidak harus memperlakukan benda-benda di stack sebagai hidup, dengan cara yang sama bahwa memperlakukan setiap objek diketahui hidup sebagai hidup. Objek di tumpukan bisa merujuk ke objek dengan alokasi heap yang perlu dipertahankan, jadi GC harus memperlakukan objek tumpukan seperti objek hidup dengan alokasi heap untuk tujuan menentukan set langsung. Tapi jelas mereka tidak diperlakukan sebagai "benda hidup" untuk tujuan memadatkan heap, karena mereka tidak berada di heap sejak awal.

Apakah itu jelas?

Eric Lippert
sumber
Eric, apakah Anda tahu apakah compiler atau jitter menggunakan immutability (mungkin jika diberlakukan dengan readonly) untuk memungkinkan pengoptimalan. Saya tidak akan membiarkan hal itu memengaruhi pilihan pada mutabilitas (saya gila untuk detail efisiensi dalam teori, tetapi dalam praktiknya langkah pertama saya menuju efisiensi selalu berusaha untuk memiliki jaminan kebenaran sesederhana mungkin dan karenanya tidak perlu buang siklus CPU dan siklus otak pada pemeriksaan dan kasus tepi, dan menjadi yang dapat diubah atau tidak berubah dengan tepat membantu di sana), tetapi itu akan melawan reaksi spontan apa pun terhadap pernyataan Anda yang tidak dapat diubah bisa lebih lambat.
Jon Hanna
@ Jon: Kompilator C # mengoptimalkan data const tetapi bukan data hanya- baca . Saya tidak tahu apakah kompiler jit melakukan optimasi caching pada field readonly.
Eric Lippert
Sayang sekali, karena saya tahu pengetahuan tentang keabadian memungkinkan untuk beberapa optimisasi, tetapi mencapai batas pengetahuan teoretis saya pada saat itu, tetapi itu adalah batas yang ingin saya kembangkan. Sementara itu "bisa lebih cepat dua arah, inilah alasannya, sekarang uji dan cari tahu mana yang berlaku dalam kasus ini" berguna untuk dapat mengatakan :)
Jon Hanna
Saya akan merekomendasikan untuk membaca simple-talk.com/dotnet/.net-framework/… dan artikel Anda sendiri (@Eric): blogs.msdn.com/b/ericlippert/archive/2010/09/30/… untuk mulai menyelam menjadi detail. Ada banyak artikel bagus lainnya. BTW, perbedaan dalam memproses 100.000 objek dalam memori kecil hampir tidak terlihat melalui beberapa overhead memori (~ 2,3 MB) untuk kelas. Ini dapat dengan mudah diperiksa dengan tes sederhana.
Nick Martyshchenko
Ya, itu jelas. Terima kasih banyak atas jawaban komprehensif Anda (ekstensif lebih baik? Google translate memberikan 2 terjemahan. Saya bermaksud mengatakan bahwa Anda meluangkan waktu untuk tidak menulis jawaban singkat, tetapi meluangkan waktu untuk menulis semua detail juga) jawaban.
Michel
23

Terkadang dengan structAnda tidak perlu memanggil konstruktor new (), dan langsung menetapkan bidang sehingga lebih cepat dari biasanya.

Contoh:

Value[] list = new Value[N];
for (int i = 0; i < N; i++)
{
    list[i].id = i;
    list[i].isValid = true;
}

sekitar 2 hingga 3 kali lebih cepat dari

Value[] list = new Value[N];
for (int i = 0; i < N; i++)
{
    list[i] = new Value(i, true);
}

dimana Valuea structdengan dua bidang ( iddan isValid).

struct Value
{
    int id;
    bool isValid;

    public Value(int i, bool isValid)
    {
        this.i = i;
        this.isValid = isValid;
    }
}

Di sisi lain adalah item perlu dipindahkan atau jenis nilai yang dipilih semua yang menyalin akan memperlambat Anda. Untuk mendapatkan jawaban yang tepat, saya curiga Anda harus membuat profil kode Anda dan mengujinya.

John Alexiou
sumber
Jelas segalanya menjadi jauh lebih cepat ketika Anda mengatur nilai-nilai di atas batas-batas asli juga.
leppie
Saya sarankan menggunakan nama selain list, mengingat kode yang ditunjukkan tidak akan berfungsi dengan file List<Value>.
supercat
7

Struktur mungkin tampak mirip dengan kelas, tetapi ada perbedaan penting yang harus Anda perhatikan. Pertama-tama, kelas adalah tipe referensi dan struct adalah tipe nilai. Dengan menggunakan struct, Anda dapat membuat objek yang berperilaku seperti tipe bawaan dan menikmati manfaatnya juga.

Saat Anda memanggil operator baru di kelas, itu akan dialokasikan di heap. Namun, saat Anda membuat instance struct, itu dibuat di stack. Ini akan menghasilkan peningkatan kinerja. Juga, Anda tidak akan berurusan dengan referensi ke sebuah instance dari sebuah struct seperti yang Anda lakukan dengan kelas. Anda akan bekerja secara langsung dengan instance struct. Karenanya, saat meneruskan struct ke suatu metode, ia diteruskan oleh nilai, bukan sebagai referensi.

Selengkapnya di sini:

http://msdn.microsoft.com/en-us/library/aa288471(VS.71).aspx

kyndigs
sumber
4
Saya tahu itu mengatakannya di MSDN, tetapi MSDN tidak menceritakan keseluruhan cerita. Stack vs. heap adalah detail implementasi dan struct tidak selalu berada di stack. Untuk hanya satu blog terbaru tentang ini, lihat: blogs.msdn.com/b/ericlippert/archive/2010/09/30/…
Anthony Pegram
"... itu dilewatkan oleh nilai ..." baik referensi dan struct diteruskan oleh nilai (kecuali jika seseorang menggunakan 'ref') - apakah nilai atau referensi yang diteruskan itu berbeda, yaitu struct diteruskan nilai demi nilai , objek kelas diteruskan referensi-oleh-nilai dan parameter bertanda ref lulus referensi-demi-referensi.
Paul Ruane
10
Artikel itu menyesatkan pada beberapa poin utama, dan saya telah meminta tim MSDN untuk merevisi atau menghapusnya.
Eric Lippert
2
@supercat: untuk membahas poin pertama Anda: poin yang lebih besar adalah dalam kode terkelola di mana nilai atau referensi ke nilai disimpan sebagian besar tidak relevan . Kami telah bekerja keras untuk membuat model memori yang sebagian besar waktu memungkinkan pengembang mengizinkan runtime untuk membuat keputusan penyimpanan cerdas atas nama mereka. Perbedaan ini sangat penting ketika kegagalan untuk memahaminya memiliki konsekuensi yang menghancurkan seperti yang terjadi di C; tidak begitu banyak di C #.
Eric Lippert
1
@supercat: untuk mengatasi poin kedua Anda, tidak ada struct yang bisa berubah sebagian besar jahat. Misalnya, void M () {S s = new S (); s.Blah (); N (s); }. Refactor to: void DoBlah (S s) {s.Blah (); } void M (S s = new S (); DoBlah (s); N (s);}. Itu baru saja memperkenalkan bug karena S adalah struct yang bisa berubah. Apakah Anda langsung melihat bug? Atau apakah fakta bahwa S adalah struct yang bisa berubah menyembunyikan bug dari Anda?
Eric Lippert
6

Array struct direpresentasikan di heap dalam blok memori yang berdekatan, sedangkan array objek direpresentasikan sebagai blok referensi yang berdekatan dengan objek aktual itu sendiri di tempat lain di heap, sehingga membutuhkan memori untuk objek dan referensi arraynya. .

Dalam hal ini, saat Anda menempatkannya dalam a List<>(dan a List<>didukung ke array), akan lebih efisien, menggunakan memori untuk menggunakan struct.

(Berhati-hatilah, bahwa array besar akan menemukan jalannya di Large Object Heap di mana, jika masa pakainya lama, dapat berdampak buruk pada manajemen memori proses Anda. Ingat, juga, bahwa memori bukan satu-satunya pertimbangan.)

Paul Ruane
sumber
Anda dapat menggunakan refkata kunci untuk menangani ini.
leppie
"Namun berhati-hatilah, array besar itu akan menemukan jalannya di Large Object Heap di mana, jika masa pakainya lama, dapat berdampak buruk pada manajemen memori proses Anda." - Saya tidak begitu yakin mengapa Anda berpikir begitu? Dialokasikan pada LOH tidak akan menimbulkan efek buruk pada manajemen memori kecuali (mungkin) itu adalah objek berumur pendek dan Anda ingin mendapatkan kembali memori dengan cepat tanpa menunggu koleksi Gen 2.
Jon Artus
@ Jon Artus: LOH tidak bisa dipadatkan. Benda apapun yang berumur panjang akan membagi LOH menjadi area memori bebas sebelum dan setelah. Memori yang berdekatan diperlukan untuk alokasi dan jika area ini tidak cukup besar untuk alokasi maka lebih banyak memori dialokasikan ke LOH (yaitu Anda akan mendapatkan fragmentasi LOH).
Paul Ruane
4

Jika mereka memiliki semantik nilai, Anda mungkin harus menggunakan struct. Jika mereka memiliki semantik referensi, Anda mungkin harus menggunakan kelas. Ada pengecualian, yang kebanyakan condong ke arah pembuatan kelas bahkan ketika ada semantik nilai, tetapi mulai dari sana.

Sedangkan untuk pengeditan kedua Anda, GC hanya menangani heap, tetapi ada lebih banyak ruang heap daripada ruang tumpukan, jadi meletakkan sesuatu di tumpukan tidak selalu menguntungkan. Selain itu, daftar tipe-struct dan daftar tipe-kelas akan ada di heap, jadi ini tidak relevan dalam kasus ini.

Edit:

Saya mulai menganggap istilah kejahatan itu berbahaya. Lagi pula, membuat kelas bisa berubah adalah ide yang buruk jika tidak diperlukan secara aktif, dan saya tidak akan mengesampingkan pernah menggunakan struct yang bisa berubah. Ini adalah ide yang buruk sehingga sering kali hampir selalu menjadi ide yang buruk, tetapi sebagian besar itu tidak sesuai dengan nilai semantik sehingga tidak masuk akal untuk menggunakan struct dalam kasus tertentu.

Mungkin ada pengecualian yang wajar dengan struct bertingkat pribadi, di mana semua penggunaan struct itu dibatasi pada cakupan yang sangat terbatas. Ini tidak berlaku di sini.

Sungguh, saya pikir "itu bermutasi jadi ini adalah tindakan yang buruk" tidak jauh lebih baik daripada membahas tentang heap dan tumpukan (yang setidaknya memiliki beberapa dampak kinerja, bahkan jika sering disalahartikan). "Ini bermutasi, jadi sepertinya tidak masuk akal untuk menganggapnya memiliki nilai semantik, jadi ini adalah struct yang buruk" hanya sedikit berbeda, tapi yang terpenting jadi saya pikir.

Jon Hanna
sumber
3

Solusi terbaik adalah mengukur, mengukur lagi, lalu mengukur lagi. Mungkin ada detail dari apa yang Anda lakukan yang mungkin membuat jawaban yang sederhana dan mudah seperti "gunakan struktur" atau "kelas penggunaan" menjadi sulit.

FMM
sumber
setuju dengan bagian ukuran, tetapi menurut saya itu adalah contoh yang lurus ke depan dan jelas, dan saya pikir mungkin beberapa hal umum dapat dikatakan tentang itu. Dan ternyata, beberapa orang melakukannya.
Michel
3

Sebuah struct, pada intinya, tidak lebih dan tidak kurang dari kumpulan bidang. Dalam .NET, mungkin saja struktur "berpura-pura" menjadi objek, dan untuk setiap jenis struktur .NET secara implisit mendefinisikan tipe objek heap dengan bidang dan metode yang sama yang - menjadi objek heap - akan berperilaku seperti objek . Variabel yang menyimpan referensi ke objek heap semacam itu (struktur "kotak") akan menunjukkan semantik referensi, tetapi variabel yang memegang struct secara langsung hanyalah kumpulan variabel.

Saya pikir banyak kebingungan struct-versus-class berasal dari fakta bahwa struktur memiliki dua kasus penggunaan yang sangat berbeda, yang seharusnya memiliki pedoman desain yang sangat berbeda, tetapi pedoman MS tidak membedakannya. Terkadang ada kebutuhan akan sesuatu yang berperilaku seperti objek; dalam hal ini, pedoman MS cukup masuk akal, meskipun "batas 16 byte" mungkin lebih seperti 24-32. Namun terkadang, yang dibutuhkan adalah agregasi variabel. Sebuah struct yang digunakan untuk tujuan itu seharusnya hanya terdiri dari sekumpulan bidang publik, dan mungkin sebuah Equalsoverride, ToStringoverride, danIEquatable(itsType).Equalspenerapan. Struktur yang digunakan sebagai agregasi bidang bukanlah objek, dan tidak boleh berpura-pura menjadi. Dari sudut pandang struktur, arti dari field seharusnya tidak lebih atau kurang dari "hal terakhir yang dituliskan pada field ini". Arti tambahan harus ditentukan oleh kode klien.

Misalnya, jika struct pengumpul variabel memiliki anggota Minimumdan Maximum, struct itu sendiri seharusnya tidak menjanjikan hal itu Minimum <= Maximum. Kode yang menerima struktur seperti parameter harus berperilaku seolah-olah diteruskan secara terpisah Minimumdan Maximumnilai. Persyaratan yang Minimumtidak lebih besar dari Maximumharus dianggap seperti persyaratan bahwa Minimumparameter tidak boleh lebih besar dari yang diteruskan secara terpisah Maximum.

Pola yang berguna untuk dipertimbangkan kadang-kadang adalah membuat ExposedHolder<T>kelas mendefinisikan sesuatu seperti:

class ExposedHolder<T>
{
  public T Value;
  ExposedHolder() { }
  ExposedHolder(T val) { Value = T; }
}

Jika seseorang memiliki List<ExposedHolder<someStruct>>, di mana someStructstruct pengumpul-variabel, ia dapat melakukan hal-hal seperti itu myList[3].Value.someField += 7;, tetapi memberikan myList[3].Valueke kode lain akan memberikan isinya Valuedaripada memberinya sarana untuk mengubahnya. Sebaliknya, jika seseorang menggunakan a List<someStruct>, itu akan perlu digunakan var temp=myList[3]; temp.someField += 7; myList[3] = temp;. Jika seseorang menggunakan tipe kelas yang bisa berubah, mengekspos konten myList[3]ke kode luar akan memerlukan penyalinan semua bidang ke objek lain. Jika seseorang menggunakan tipe kelas yang tidak bisa diubah, atau struct "object-style", maka perlu untuk membuat sebuah instance baru yang seperti myList[3]kecuali someFieldyang berbeda, dan kemudian menyimpan instance baru itu ke dalam daftar.

Satu catatan tambahan: Jika Anda menyimpan banyak hal yang serupa, mungkin baik untuk menyimpannya dalam susunan struktur yang mungkin bersarang, lebih disukai mencoba untuk menjaga ukuran setiap larik antara 1K dan 64K atau lebih. Array struktur adalah khusus, dalam pengindeksan itu akan menghasilkan referensi langsung ke struktur di dalamnya, sehingga seseorang dapat mengatakan "a [12] .x = 5;". Meskipun seseorang dapat mendefinisikan objek mirip array, C # tidak mengizinkan mereka untuk berbagi sintaks seperti itu dengan array.

supercat
sumber
1

Gunakan kelas.

Pada catatan umum. Mengapa tidak memperbarui nilai b saat Anda membuatnya?

Preet Sangha
sumber
1

Dari perspektif c ++ saya setuju bahwa memodifikasi properti struct lebih lambat dibandingkan dengan kelas. Tapi saya pikir mereka akan lebih cepat dibaca karena struct dialokasikan pada tumpukan daripada heap. Membaca data dari heap membutuhkan lebih banyak pemeriksaan daripada dari tumpukan.

Robert
sumber
1

Nah, jika Anda menggunakan struct afterall, maka singkirkan string dan gunakan char ukuran tetap atau buffer byte.

Itu re: kinerja.

Daniel Mošmondor
sumber