Kesalahpahaman untuk pengembang C # pemula. Mencoba menjawab pertanyaan standar

Baru-baru ini saya berkesempatan untuk mengobrol dengan sejumlah besar pengembang C # pemula. Banyak dari mereka yang tertarik dengan bahasa dan platform, dan ini sangat keren. Di antara junior hijau obskurantisme tersebar luas tentang hal-hal yang jelas (hanya membaca buku tentang ingatan). Dan ini juga mendorong saya untuk membuat artikel ini. Artikel ini terutama ditujukan untuk pengembang pemula, tapi saya pikir banyak fakta akan berguna untuk melatih para insinyur. Yah, kesalahan yang paling jelas dan tidak menarik, tentu saja, dihilangkan. Inilah yang paling menarik dan penting, terutama dari sudut pandang lulus wawancara.



# 1 Mantra tentang 3 generasi dalam situasi apa pun


Ini lebih tidak akurat daripada kesalahan. Pertanyaan tentang "pengumpul sampah di C #" untuk pengembang telah menjadi pertanyaan klasik dan sedikit orang akan mulai menjawab dengan cerdas tentang konsep generasi. Namun, untuk beberapa alasan, beberapa orang memperhatikan fakta bahwa pengumpul sampah yang hebat dan mengerikan adalah bagian dari runtime. Oleh karena itu, saya akan menjelaskan bahwa itu bukan jari, dan akan bertanya seperti apa lingkungan runtime yang terlibat. Untuk kueri "pengumpul sampah di c #", lebih dari banyak informasi serupa dapat ditemukan di Internet. Namun, beberapa orang menyebutkan bahwa informasi ini merujuk pada CLR / CoreCLR (sebagai aturan). Tapi jangan lupa tentang Mono, runtime yang ringan, fleksibel, dan tertanam yang telah menempati ceruknya dalam pengembangan ponsel (Unity, Xamarin) dan digunakan di Blazor. Dan untuk masing-masing pengembang, saya akan menyarankan Anda untuk menanyakan tentang detail perangkat perakitan di Mono. Misalnya, atas permintaan "generasi pengumpul sampah mono", Anda dapat melihat bahwa hanya ada dua generasi - pembibitan dan generasi tua (dalam pengumpul sampah baru dan modis - SGen ).

# 2 Mantra tentang 2 tahap pengumpulan sampah dalam situasi apa pun


Belum lama berselang, sumber-sumber pengumpul sampah disembunyikan dari semua orang. Namun, minat pada struktur internal platform selalu. Karena itu, informasi diekstraksi dengan cara yang berbeda. Dan beberapa ketidakakuratan dalam rekayasa balik kolektor menyebabkan mitos bahwa kolektor bekerja dalam 2 tahap: penandaan dan pembersihan. Atau bahkan lebih buruk lagi, 3 tahap - menandai, membersihkan, kompresi.

Namun, semuanya berubah ketika orang-orang api melepaskan perang dengan munculnya CoreCLR dan kode sumber untuk kolektor. Kode kompiler untuk CoreCLR diambil seluruhnya dari versi CLR. Tidak ada yang menulisnya dari awal, masing-masing, hampir semua yang dapat dipelajari dari kode sumber CoreCLR akan berlaku untuk CLR juga. Sekarang, untuk memahami cara kerja sesuatu, buka saja github dan temukan di kode sumber atau baca readme . Di sana Anda dapat melihat bahwa ada 5 fase: menandai, merencanakan, memperbarui tautan, memadatkan (penghapusan dengan relokasi) dan penghapusan tanpa relokasi (ini sulit untuk diterjemahkan). Tetapi secara formal dapat dibagi menjadi 3 tahap - penandaan, perencanaan, pembersihan.

Pada tahap penandaan, ternyata benda mana yang tidak boleh dikumpulkan oleh pengumpul.
Pada tahap perencanaan, berbagai indikator dari kondisi memori saat ini dihitung dan data yang diperlukan pada tahap pembersihan dikumpulkan. Berkat informasi yang diterima pada tahap ini, keputusan dibuat tentang perlunya pemadatan (defragmentasi), juga menghitung berapa banyak yang Anda butuhkan untuk memindahkan objek, dll.

Dan pada tahap pembersihan , tergantung pada kebutuhan untuk pemadatan, tautan dapat diperbarui dan dipadatkan atau dihapus tanpa bergerak.

# 3 Mengalokasikan memori pada heap secepat di stack


Sekali lagi, ketidakakuratan dan bukannya ketidakbenaran mutlak. Dalam kasus umum, tentu saja, perbedaan kecepatan alokasi memori minimal. Memang, dalam kasus terbaik, dengan alokasi bump pointer , alokasi memori hanyalah pergeseran pointer, seperti pada stack. Namun, faktor-faktor seperti menetapkan objek baru ke bidang lama (yang akan mempengaruhi penghalang tulis , memperbarui tabel kartu - mekanisme yang memungkinkan Anda untuk melacak tautan dari generasi yang lebih tua ke yang lebih muda), keberadaan finalizer (Anda harus menambahkan jenis ke antrian yang sesuai) dapat mempengaruhi alokasi memori pada tumpukan. dan lain-lain. Mungkin juga bahwa objek akan direkam di salah satu lubang bebas di tumpukan (setelah perakitan tanpa defragmentasi). Dan menemukan lubang seperti itu, meskipun cepat, jelas lebih lambat daripada pergeseran pointer sederhana. Yah, tentu saja, setiap objek yang dibuat membawa pengumpulan sampah berikutnya lebih dekat. Dan dalam prosedur selanjutnya untuk mengalokasikan memori, itu bisa terjadi. Yang, tentu saja, akan memakan waktu.

# 4 Definisi referensi, tipe dan kemasan yang bermakna melalui konsep tumpukan dan tumpukan


Klasik yang benar, yang, untungnya, tidak begitu umum.

Jenis referensi terletak di heap. Signifikan pada tumpukan. Tentunya banyak yang sudah sering mendengar definisi ini. Tapi ini tidak hanya kebenaran parsial, jadi mendefinisikan konsep melalui abstraksi yang bocor bukanlah ide yang baik. Untuk semua definisi, saya sarankan Anda merujuk ke standar CLI - ECMA 335 . Pertama, perlu diklarifikasi bahwa tipe menggambarkan nilai. Jadi, tipe referensi didefinisikan sebagai berikut - nilai yang dijelaskan oleh tipe referensi (tautan) menunjukkan lokasi nilai lain. Untuk tipe yang signifikan, nilai yang dideskripsikan olehnya adalah otonom (mandiri). Tentang di mana ini atau jenis kata-kata itu berada. Ini adalah abstraksi yang bocor yang harus Anda ketahui.

Jenis yang signifikan dapat ditemukan:

  1. Dalam memori dinamis (tumpukan), jika itu adalah bagian dari objek yang terletak di tumpukan, atau dalam kasus kemasan;
  2. Pada stack, jika itu adalah variabel lokal / argumen / nilai pengembalian metode;
  3. Dalam register, jika memungkinkan ukuran tipe yang signifikan dan kondisi lainnya.

Jenis referensi, yaitu nilai yang menjadi titik tautan, saat ini berada di heap.

Tautan itu sendiri dapat ditempatkan di tempat yang sama dengan tipe signifikan.

Pengemasan juga tidak ditentukan melalui lokasi penyimpanan. Pertimbangkan contoh singkat.

Kode C #
public struct MyStruct { public int justField; } public class MyClass { public MyStruct justStruct; } public static void Main() { MyClass instance = new MyClass(); object boxed = instance.justStruct; } 


Dan kode IL yang sesuai untuk metode Utama

Kode IL
  1: nop 2: newobj instance void C/MyClass::.ctor() 3: stloc.0 4: ldloc.0 5: ldfld valuetype C/MyStruct C/MyClass::justStruct 6: box C/MyStruct 7: stloc.1 8: ret 


Karena tipe signifikan adalah bagian dari referensi, jelas bahwa itu akan ditempatkan di heap. Dan baris keenam memperjelas bahwa kita berurusan dengan pengemasan. Dengan demikian, definisi khas "menyalin dari tumpukan ke tumpukan" gagal.

Untuk menentukan apa paket itu, sebagai permulaan, perlu dikatakan bahwa untuk setiap tipe signifikan, CTS (sistem tipe umum) mendefinisikan tipe referensi, yang disebut tipe paket. Jadi, pengemasan adalah operasi pada tipe signifikan yang menciptakan nilai dari tipe paket yang sesuai yang mengandung salinan bitwise dari nilai asli.

# 4 Acara - mekanisme terpisah


Peristiwa ada dari versi pertama bahasa dan pertanyaan tentang mereka jauh lebih umum daripada peristiwa itu sendiri. Namun, perlu dipahami dan mengetahui apa itu, karena mekanisme ini memungkinkan Anda untuk menulis kode yang sangat longgar, yang kadang-kadang berguna.

Sayangnya, seringkali suatu peristiwa dipahami sebagai instrumen, jenis, mekanisme yang terpisah. Ini terutama difasilitasi oleh tipe dari BCL EventHandler , yang namanya menunjukkan bahwa itu adalah sesuatu yang terpisah.

Mendefinisikan suatu acara harus dimulai dengan mendefinisikan properti. Saya telah lama menggambar analogi seperti itu untuk diri saya sendiri, dan baru-baru ini melihat bahwa analog itu diambil dalam spesifikasi CLI.

Properti mendefinisikan nilai yang disebutkan dan metode yang mengaksesnya. Itu terdengar sangat jelas. Kami lolos ke acara. CTS mendukung acara serta properti, TETAPI metode untuk akses berbeda dan termasuk metode untuk berlangganan dan berhenti berlangganan dari suatu acara. Dari spesifikasi bahasa C #, kelas mendefinisikan acara ... yang mengingatkan pada deklarasi lapangan dengan penambahan kata kunci acara. Jenis deklarasi ini haruslah tipe delegasi. Berkat standar CLI untuk definisi.

Jadi, ini berarti bahwa acara tersebut tidak lebih dari delegasi yang memaparkan hanya sebagian dari fungsi delegasi - menambahkan delegasi lain ke daftar untuk dieksekusi, menghapusnya dari daftar ini. Di dalam kelas, acara ini tidak berbeda dengan bidang tipe delegasi sederhana.

# 5 Sumber daya yang dikelola dan tidak dikelola. Finalizers dan IDisposable


Ada kebingungan mutlak ketika berhadapan dengan sumber daya ini. Ini sebagian besar difasilitasi oleh Internet dengan seribu artikel tentang penerapan pola Buang yang benar. Sebenarnya, tidak ada yang kriminal dalam pola ini - metode templat yang dimodifikasi untuk kasus tertentu. Tetapi pertanyaannya adalah apakah itu diperlukan? Untuk beberapa alasan, beberapa orang memiliki keinginan yang tak tertahankan untuk menerapkan finalizer untuk setiap bersin. Kemungkinan besar, alasan untuk ini bukanlah pemahaman penuh tentang apa "sumber daya yang tidak dikelola" itu. Dan garis-garis tentang fakta bahwa dalam finalizer, sebagai suatu peraturan, sumber daya yang tidak dikelola dilepaskan karena pemahaman yang tidak lengkap ini, melewati dan tidak tetap berada di kepala.

Sumber daya yang tidak dikelola adalah sumber daya yang tidak dikelola (betapapun anehnya). Sumber daya yang dikelola , pada gilirannya, adalah sumber daya yang dialokasikan dan dirilis oleh CLI secara otomatis melalui proses yang disebut pengumpulan sampah. Saya dengan berani menghapus definisi ini dari standar CLI. Tetapi jika Anda mencoba menjelaskan secara lebih sederhana, sumber daya yang tidak dikelola adalah sumber yang tidak diketahui oleh pemulung. (Secara tegas, kami dapat memberi informasi kepada kolektor tentang sumber daya tersebut menggunakan GC.AddMemoryPressure dan GC.RemoveMemoryPressure, ini dapat memengaruhi penyetelan internal kolektor). Karenanya, dia tidak akan dapat mengurus pembebasan mereka sendiri, dan oleh karena itu kita harus melakukannya untuknya. Dan mungkin ada banyak pendekatan untuk ini. Dan agar kode tidak menyilaukan dengan keragaman imajinasi pengembang, 2 pendekatan yang diterima secara umum digunakan.

  1. Antarmuka IDisposable (dan versi asinkron dari IAsyncDisposable). Itu dipantau oleh semua penganalisa kode, jadi sulit untuk melupakan panggilannya. Menyediakan metode tunggal - Buang. Dan dukungan kompiler adalah pernyataan using. Calon yang sangat baik untuk badan metode Buang adalah memanggil metode serupa dari salah satu bidang di kelas atau untuk melepaskan sumber daya yang tidak dikelola. Dipanggil secara eksplisit oleh pengguna kelas. Kehadiran antarmuka ini di kelas menyiratkan bahwa setelah menyelesaikan pekerjaan dengan instance, Anda perlu memanggil metode ini.
  2. Finalizer Pada intinya adalah asuransi. Diminta secara implisit, pada waktu yang tidak ditentukan, selama pengumpulan sampah. Memperlambat alokasi memori, pengumpul sampah, memperpanjang umur objek setidaknya sampai bangunan berikutnya, atau bahkan lebih lama, tetapi itu disebut dengan sendirinya, bahkan jika tidak ada yang memanggilnya. Karena sifatnya yang non-deterministik, hanya sumber daya yang tidak dikelola yang harus dibebaskan di dalamnya. Anda juga dapat menemukan contoh di mana finalizer digunakan untuk menghidupkan kembali objek dan mengatur kumpulan objek dengan cara ini. Namun, implementasi kumpulan objek seperti itu jelas merupakan ide yang buruk. Seperti mencoba masuk, melempar pengecualian, mengakses database dan ribuan tindakan serupa.

Dan Anda dapat dengan mudah membayangkan situasi ketika menulis perpustakaan penting untuk kinerja, yang secara internal menggunakan sumber daya yang tidak dikelola, yang dapat ditangani hanya dengan penanganan yang kompeten dari sumber daya ini, membebaskan memori dengan hati-hati secara manual. Saat menulis perpustakaan berkinerja tinggi, OOP, dukungan, dan yang lainnya menyukainya, berjalan di pinggir jalan.

Dan bertentangan dengan pernyataan bahwa Buang melanggar konsep di mana CLR akan melakukan segalanya untuk kita, memaksa kita untuk melakukan sesuatu sendiri, mengingat sesuatu, dll., Saya akan mengatakan yang berikut. Ketika bekerja dengan sumber daya yang tidak dikelola, Anda harus siap bahwa mereka tidak dikelola oleh orang lain selain Anda. Dan secara umum, situasi di mana sumber daya ini akan digunakan dalam perusahaan hampir tidak pernah ditemukan. Dan dalam kebanyakan kasus, Anda bisa bertahan dengan kelas pembungkus yang indah, seperti SafeHandle, yang menyediakan finalisasi sumber daya yang kritis, mencegah perakitan prematur mereka.

Jika, karena satu dan lain alasan, ada banyak sumber daya dalam aplikasi Anda yang memerlukan langkah tambahan untuk membebaskan, maka Anda harus melihat pada pola JetBrains yang sangat baik, Lifetime. Tapi Anda tidak boleh menggunakannya saat Anda melihat objek IDisposable pertama.

# 6 Stream stack, stack panggilan, tumpukan komputasi dan
  Stack <T> 


Paragraf terakhir menambahkan tawa demi itu, saya tidak berpikir bahwa ada orang yang menghubungkan yang terakhir dengan yang sebelumnya. Namun, ada banyak kebingungan tentang apa aliran stack, tumpukan panggilan, dan tumpukan komputasi.

Tumpukan panggilan adalah struktur data, yaitu tumpukan, untuk menyimpan alamat kembali, untuk kembali dari fungsi. Tumpukan panggilan adalah konsep yang lebih logis. Itu tidak mengatur di mana dan bagaimana informasi harus disimpan untuk dikembalikan. Ternyata tumpukan panggilan adalah tumpukan yang paling umum dan asli yaitu. Stack (lelucon). Variabel lokal disimpan di dalamnya, parameter dilewatkan, dan alamat pengirim disimpan di dalamnya ketika instruksi CALL dan interupsi dipanggil, yang selanjutnya digunakan oleh instruksi RET untuk kembali dari fungsi / interupsi. Silakan. Salah satu lelucon utama aliran adalah pointer ke instruksi, yang dieksekusi lebih lanjut. Sebuah utas pada gilirannya mengeksekusi instruksi yang menggabungkan fungsi. Dengan demikian, setiap utas memiliki tumpukan panggilan. Dengan demikian, ternyata tumpukan aliran adalah tumpukan panggilan. Yaitu, tumpukan panggilan dari aliran ini. Secara umum, ini juga disebut dengan nama lain: tumpukan perangkat lunak, tumpukan mesin.

Itu dianggap secara rinci dalam artikel sebelumnya .
Juga, definisi tumpukan panggilan digunakan untuk menunjukkan rantai panggilan metode tertentu dalam bahasa tertentu.

Tumpukan komputasi (tumpukan evaluasi) . Seperti yang Anda ketahui, kode C # dikompilasi menjadi kode IL, yang merupakan bagian dari DLL yang dihasilkan (dalam kasus paling umum). Dan tepat di jantung runtime yang menyerap DLL kami dan mengeksekusi kode IL adalah mesin stack. Hampir semua instruksi IL beroperasi dengan tumpukan tertentu. Sebagai contoh, ldloc memuat variabel lokal di bawah indeks spesifik ke stack. Di sini, tumpukan mengacu pada tumpukan virtual tertentu, karena pada akhirnya variabel ini dapat dengan probabilitas tinggi berada dalam register. Aritmatika, logis, dan instruksi IL lainnya beroperasi pada variabel dari stack dan meletakkan hasilnya di sana. Artinya, perhitungan dilakukan melalui tumpukan ini. Jadi, ternyata tumpukan komputasi adalah abstraksi dalam runtime. Omong-omong, banyak mesin virtual berbasis stack.

# 7 Lebih banyak utas - kode lebih cepat


Tampaknya secara intuitif bahwa pemrosesan data secara paralel akan lebih cepat daripada secara bergantian. Oleh karena itu, dipersenjatai dengan pengetahuan tentang bekerja dengan utas, banyak yang mencoba untuk memparalelkan setiap siklus dan perhitungan. Hampir semua orang sudah tahu tentang overhead, yang berkontribusi pada pembuatan utas, sehingga mereka menggunakan utas dari ThreadPool dan Tugas terkenal. Tetapi overhead menciptakan aliran jauh dari akhir. Di sini kita berhadapan dengan abstraksi lain yang bocor, mekanisme yang digunakan prosesor untuk meningkatkan kinerja - cache. Dan seperti yang sering terjadi, cache adalah blade bermata dua. Di satu sisi, itu secara signifikan mempercepat pekerjaan dengan akses berurutan ke data dari satu aliran. Tetapi di sisi lain, ketika beberapa utas bekerja, bahkan tanpa perlu menyinkronkannya, cache tidak hanya tidak membantu, tetapi juga memperlambat kerja. Waktu tambahan dihabiskan untuk pembatalan cache, mis. memelihara data yang relevan. Dan jangan meremehkan masalah ini, yang awalnya tampak seperti hal sepele. Algoritma cache-efisien akan mengeksekusi satu thread lebih cepat daripada algoritma multi-threaded, di mana cache digunakan secara tidak efisien.

Juga mencoba bekerja dengan drive dari banyak utas adalah bunuh diri. Disk sudah menjadi faktor penghambat dalam banyak program yang bekerja dengannya. Jika Anda mencoba mengatasinya dari banyak utas, maka Anda harus melupakan kecepatan.

Untuk semua definisi, saya sarankan menghubungi di sini:

Spesifikasi Bahasa C # - ECMA-334
Sumber yang bagus:
Konrad Kokosa - Manajemen Memori NET. Pro
Spesifikasi CLI - ECMA-335
Pengembang CoreCLR tentang runtime - Book Of The Runtime
Dari Stanislav Sidristy tentang finalisasi dan lainnya - .NET Platform Architecture

Source: https://habr.com/ru/post/id463213/


All Articles