Apa kompleksitas pemrograman yang tidak dikelola dengan memori?

24

Atau dengan kata lain, masalah spesifik apa yang dipecahkan oleh pengumpulan sampah otomatis? Saya tidak pernah melakukan pemrograman tingkat rendah, jadi saya tidak tahu betapa rumitnya membebaskan sumber daya.

Jenis bug yang ditangani oleh GC tampaknya (setidaknya bagi pengamat eksternal) hal-hal yang tidak dapat dilakukan oleh programmer yang mengerti bahasa, perpustakaan, konsep, idiom, dll. Tapi saya bisa saja salah: apakah penanganan memori manual pada hakekatnya rumit?

vemv
sumber
3
Perluas untuk memberi tahu kami bagaimana pertanyaan Anda tidak dijawab oleh artikel Wikipedia tentang pengumpulan garbace dan lebih khusus lagi bagian tentang manfaatnya
yannis
Manfaat lain adalah keamanan, misalnya buffer overruns sangat dapat dieksploitasi dan banyak kerentanan keamanan lainnya muncul dari manajemen memori (mis).
StuperUser
7
@StuperUser: Itu tidak ada hubungannya dengan asal memori. Anda dapat buffer memori overrun yang berasal dari GC baik-baik saja. Fakta bahwa bahasa-bahasa GC biasanya mencegah hal ini adalah ortogonal, dan bahasa-bahasa yang kurang dari tiga puluh tahun di belakang teknologi GC yang Anda bandingkan juga menawarkan perlindungan buffer overrun.
DeadMG

Jawaban:

29

Saya tidak pernah melakukan pemrograman tingkat rendah, jadi saya tidak tahu betapa rumitnya membebaskan sumber daya.

Lucu bagaimana definisi "level rendah" berubah seiring waktu. Ketika saya pertama kali belajar memprogram, bahasa apa pun yang menyediakan model tumpukan standar yang memungkinkan pengalokasian sederhana / pola bebas mungkin dianggap tingkat tinggi memang. Dalam pemrograman tingkat rendah , Anda harus melacak sendiri ingatan, (bukan alokasi, tetapi lokasi ingatan itu sendiri!), Atau menulis pengalokasi tumpukan Anda sendiri jika Anda merasa benar-benar mewah.

Karena itu, sama sekali tidak ada yang menakutkan atau "rumit" tentang itu. Ingat ketika Anda masih anak-anak dan ibu Anda mengatakan kepada Anda untuk menyimpan mainan Anda ketika Anda selesai bermain dengan mereka, bahwa dia bukan pelayan Anda dan tidak akan membersihkan kamar Anda untuk Anda? Manajemen memori hanyalah prinsip yang sama yang diterapkan pada kode. (GC seperti memiliki pelayan yang akan membersihkan setelah Anda, tetapi dia sangat malas dan sedikit tidak mengerti.) Prinsipnya sederhana: Setiap variabel dalam kode Anda memiliki satu dan hanya satu pemilik, dan itu adalah tanggung jawab pemilik itu untuk bebaskan memori variabel saat tidak lagi diperlukan. ( Prinsip Kepemilikan Tunggal)) Ini membutuhkan satu panggilan per alokasi, dan ada beberapa skema yang mengotomatiskan kepemilikan dan pembersihan dalam satu atau lain cara sehingga Anda bahkan tidak perlu menulis panggilan itu ke dalam kode Anda sendiri.

Pengumpulan sampah seharusnya menyelesaikan dua masalah. Itu selalu melakukan pekerjaan yang sangat buruk di salah satu dari mereka, dan tergantung pada implementasinya mungkin atau tidak mungkin melakukannya dengan baik dengan yang lain. Masalahnya adalah kebocoran memori (menyimpan memori setelah Anda selesai dengan itu) dan menggantung referensi (membebaskan memori sebelum Anda selesai dengan itu.) Mari kita lihat kedua masalah:

Referensi yang menggantung: Diskusikan ini dulu karena ini benar-benar serius. Anda punya dua petunjuk untuk objek yang sama. Anda membebaskan salah satu dari mereka dan tidak memperhatikan yang lain. Kemudian pada suatu saat nanti Anda mencoba membaca (atau menulis atau membebaskan) yang kedua. Terjadi perilaku yang tidak terdefinisi. Jika Anda tidak menyadarinya, Anda dapat dengan mudah merusak memori Anda. Pengumpulan sampah seharusnya membuat masalah ini menjadi tidak mungkin dengan memastikan bahwa tidak ada yang pernah dibebaskan sampai semua referensi tentangnya hilang. Dalam bahasa yang dikelola sepenuhnya, ini hampir berhasil, sampai Anda harus berurusan dengan sumber daya memori eksternal yang tidak dikelola. Kemudian langsung kembali ke titik 1. Dan dalam bahasa yang tidak dikelola, semuanya masih lebih rumit. (Cari-cari di Mozilla '

Untungnya, menangani masalah ini pada dasarnya adalah masalah yang terpecahkan. Anda tidak memerlukan pengumpul sampah, Anda memerlukan manajer memori debugging. Saya menggunakan Delphi, misalnya, dan dengan satu perpustakaan eksternal dan arahan kompiler sederhana saya dapat mengatur pengalokasi ke "Mode Debug Penuh." Ini menambahkan overhead kinerja yang dapat diabaikan (kurang dari 5%) sebagai imbalan untuk mengaktifkan beberapa fitur yang melacak memori yang digunakan. Jika saya membebaskan suatu objek, ia mengisi ingatannya dengan0x80byte (mudah dikenali di debugger) dan jika saya pernah mencoba memanggil metode virtual (termasuk destruktor) pada objek yang dibebaskan, itu memperhatikan dan mengganggu program dengan kotak kesalahan dengan tiga jejak tumpukan - ketika objek dibuat, ketika itu dibebaskan, dan di mana saya sekarang - ditambah beberapa informasi berguna lainnya, maka menimbulkan pengecualian. Ini jelas tidak cocok untuk rilis build, tetapi membuat pelacakan dan memperbaiki masalah referensi yang menggantung menjadi sepele.

Masalah kedua adalah kebocoran memori. Inilah yang terjadi ketika Anda terus memegang memori yang dialokasikan ketika Anda tidak lagi membutuhkannya. Itu bisa terjadi dalam bahasa apa pun, dengan atau tanpa pengumpulan sampah, dan hanya dapat diperbaiki dengan menuliskan kode Anda dengan benar. Pengumpulan sampah membantu mengurangi satu bentuk spesifik dari kebocoran memori, jenis yang terjadi ketika Anda tidak memiliki referensi yang valid ke sepotong memori yang belum dibebaskan, yang berarti memori tetap dialokasikan hingga program berakhir. Sayangnya, satu-satunya cara untuk mencapai ini secara otomatis adalah dengan mengubah setiap alokasi menjadi kebocoran memori!

Saya mungkin akan dicek oleh pendukung GC jika saya mencoba mengatakan sesuatu seperti itu, jadi izinkan saya menjelaskannya. Ingatlah bahwa definisi kebocoran memori bertahan pada memori yang dialokasikan ketika Anda tidak lagi membutuhkannya. Selain tidak memiliki referensi ke sesuatu, Anda juga dapat membocorkan memori dengan memiliki referensi yang tidak perlu untuk itu, seperti memegangnya di objek wadah ketika Anda seharusnya membebaskannya. Saya telah melihat beberapa kebocoran memori yang disebabkan oleh melakukan ini, dan mereka sangat sulit untuk dilacak apakah Anda memiliki GC atau tidak, karena melibatkan referensi yang benar-benar valid ke memori dan tidak ada "bug" yang jelas untuk alat debugging untuk menangkap. Sejauh yang saya tahu, tidak ada alat otomatis yang memungkinkan Anda untuk menangkap kebocoran memori jenis ini.

Jadi seorang pemulung hanya memperhatikan sendiri berbagai kebocoran memori yang tidak ada referensi, karena itu adalah satu-satunya tipe yang dapat ditangani secara otomatis. Jika itu bisa menonton semua referensi Anda untuk semuanya dan membebaskan setiap objek segera setelah nol referensi menunjuk ke sana, itu akan sempurna, setidaknya berkaitan dengan masalah tanpa referensi. Melakukan ini secara otomatis disebut penghitungan referensi, dan dapat dilakukan dalam beberapa situasi terbatas, tetapi memiliki masalah sendiri untuk dihadapi. (Misalnya, objek A memegang referensi ke objek B, yang memegang referensi ke objek A. Dalam skema penghitungan referensi, tidak ada objek yang dapat dibebaskan secara otomatis, bahkan ketika tidak ada referensi eksternal baik A atau B.) Jadi pengumpul sampah menggunakan tracingsebagai gantinya: Mulailah dengan satu set objek yang dikenal baik, temukan semua objek yang mereka rujuk, temukan semua objek yang mereka rujuk, dan seterusnya secara rekursif sampai Anda telah menemukan segalanya. Apa pun yang tidak ditemukan dalam proses penelusuran adalah sampah dan dapat dibuang. (Melakukan ini dengan sukses, tentu saja, membutuhkan bahasa yang dikelola yang menempatkan batasan tertentu pada sistem jenis untuk memastikan bahwa pengumpul sampah yang melacak selalu dapat mengetahui perbedaan antara referensi dan beberapa memori acak yang kebetulan terlihat seperti penunjuk.)

Ada dua masalah dengan penelusuran. Pertama, ini lambat, dan ketika sedang terjadi program harus lebih atau kurang dijeda untuk menghindari kondisi balapan. Ini dapat menyebabkan cegukan eksekusi yang nyata ketika program seharusnya berinteraksi dengan pengguna, atau kinerja macet di aplikasi server. Ini dapat dikurangi dengan berbagai teknik, seperti memecah memori yang dialokasikan menjadi "generasi" dengan prinsip bahwa jika alokasi tidak dikumpulkan pertama kali Anda mencoba, kemungkinan akan bertahan untuk sementara waktu. Kerangka .NET dan JVM menggunakan pengumpul sampah generasi.

Sayangnya, ini masuk ke masalah kedua: memori tidak dibebaskan ketika Anda selesai dengan itu. Kecuali jika penelusuran berjalan segera setelah Anda selesai dengan suatu objek, itu akan bertahan sampai jejak berikutnya, atau bahkan lebih lama jika itu melewati generasi pertama. Bahkan, salah satu penjelasan terbaik dari .NET sampah yang saya lihat menjelaskan bahwa, untuk membuat proses secepat mungkin, GC harus menunda pengumpulan selama mungkin! Jadi masalah kebocoran memori "terpecahkan" agak aneh dengan membocorkan memori sebanyak mungkin selama mungkin! Inilah yang saya maksud ketika saya mengatakan bahwa GC mengubah setiap alokasi menjadi kebocoran memori. Bahkan, tidak ada jaminan bahwa objek yang diberikan akan pernah dikumpulkan.

Mengapa ini menjadi masalah, ketika memori masih dapat direklamasi saat dibutuhkan? Untuk beberapa alasan. Pertama, bayangkan mengalokasikan objek besar (bitmap, misalnya,) yang membutuhkan banyak memori. Dan segera setelah Anda selesai dengan itu, Anda membutuhkan objek besar lain yang mengambil jumlah memori yang sama (atau hampir sama). Jika objek pertama telah dibebaskan, yang kedua dapat menggunakan kembali ingatannya. Tetapi pada sistem pengumpulan sampah, Anda mungkin masih menunggu jejak berikutnya untuk dijalankan, dan akhirnya Anda membuang-buang memori untuk objek besar kedua yang tidak perlu. Ini pada dasarnya adalah kondisi balapan.

Kedua, menahan memori secara tidak perlu, terutama dalam jumlah besar, dapat menyebabkan masalah dalam sistem multitasking modern. Jika Anda mengambil terlalu banyak memori fisik, ini dapat menyebabkan program Anda atau program lain harus membuka halaman (menukar sebagian memori mereka ke disk) yang benar-benar memperlambat segalanya. Untuk sistem tertentu, seperti server, paging tidak hanya dapat memperlambat sistem, tetapi juga dapat merusak semua hal jika sedang dimuat.

Seperti masalah referensi yang menggantung, masalah tanpa referensi dapat diselesaikan dengan manajer memori debugging. Sekali lagi, saya akan menyebutkan Mode Debug Penuh dari manajer memori FastMM Delphi, karena itu yang paling saya kenal. (Saya yakin sistem serupa ada untuk bahasa lain.)

Ketika sebuah program yang berjalan di bawah FastMM berakhir, Anda dapat secara opsional melaporkan keberadaan semua alokasi yang tidak pernah dibebaskan. Full Debug Mode mengambil selangkah lebih maju: ia dapat menyimpan file ke disk yang tidak hanya berisi jenis alokasi, tetapi juga jejak tumpukan dari saat dialokasikan dan info debug lainnya, untuk setiap alokasi yang bocor. Hal ini membuat pelacakan memori yang tidak ada referensi menjadi sepele.

Ketika Anda benar-benar melihatnya, pengumpulan sampah mungkin atau mungkin tidak berhasil mencegah referensi yang menggantung, dan secara universal melakukan pekerjaan yang buruk dalam menangani kebocoran memori. Satu kelebihannya, pada kenyataannya, bukan pengumpulan sampah itu sendiri, tetapi efek samping: itu menyediakan cara otomatis untuk melakukan pemadatan tumpukan. Ini dapat mencegah masalah misterius (kehabisan memori melalui fragmentasi tumpukan) yang dapat membunuh program yang berjalan terus-menerus untuk waktu yang lama dan memiliki churn memori tingkat tinggi, dan pemadatan tumpukan hampir tidak mungkin tanpa pengumpulan sampah. Namun, setiap pengalokasi memori yang baik hari ini menggunakan ember untuk meminimalkan fragmentasi, yang berarti fragmentasi hanya benar-benar menjadi masalah dalam keadaan ekstrem. Untuk program di mana tumpukan fragmentasi cenderung menjadi masalah, itu Dianjurkan untuk menggunakan pemulung sampah. Tapi IMO dalam kasus lain, penggunaan pengumpulan sampah adalah optimasi prematur, dan solusi yang lebih baik ada untuk masalah yang "diselesaikan."

Mason Wheeler
sumber
5
Saya suka jawaban ini - saya terus membacanya setiap saat. Tidak dapat memberikan komentar yang relevan sehingga yang bisa saya katakan adalah - terima kasih.
vemv
3
Saya ingin menunjukkan bahwa ya, GC cenderung "membocorkan" memori (setidaknya untuk sementara waktu), tetapi ini bukan masalah karena akan mengumpulkan memori ketika pengalokasi memori tidak dapat mengalokasikan memori sebelum pengumpulan. Dengan bahasa non-GC, kebocoran selalu menyebabkan kebocoran, artinya Anda sebenarnya dapat kehabisan memori karena terlalu banyak memori yang tidak terkumpul. "pengumpulan sampah adalah optimasi dini" ... GC bukan optimasi dan tidak dirancang untuk itu. Kalau tidak, jawaban yang bagus.
Thomas Eding
7
@ThomasEding: GC tentu saja merupakan optimasi; itu mengoptimalkan untuk upaya programmer minimal, dengan mengorbankan kinerja dan berbagai metrik kualitas program lainnya.
Mason Wheeler
5
Lucu bahwa Anda menunjuk pelacak bug Mozilla pada satu titik, karena Mozilla telah sampai pada kesimpulan yang sangat berbeda. Firefox telah dan terus memiliki masalah keamanan yang tak terhitung jumlahnya yang berasal dari kesalahan manajemen memori. Perhatikan bahwa ini bukan tentang betapa mudahnya untuk memperbaiki kesalahan setelah terdeteksi --- biasanya kerusakan sudah dilakukan pada saat pengembang menyadari masalah ini. Mozilla mendanai bahasa pemrograman Rust secara tepat untuk membantu mencegah kesalahan tersebut diperkenalkan.
1
Meskipun demikian, Rust tidak menggunakan pengumpulan sampah, ia menggunakan penghitungan referensi dengan tepat seperti yang dijelaskan Mason, hanya dengan pemeriksaan waktu kompilasi yang ekstensif daripada harus menggunakan debugger untuk mendeteksi kesalahan pada saat run-time ...
Sean Burton
13

Mempertimbangkan teknik manajemen memori non-sampah yang dikumpulkan dari era yang setara sebagai pengumpul sampah yang digunakan dalam sistem populer saat ini, seperti RAII C ++. Mengingat pendekatan ini, maka biaya untuk tidak menggunakan pengumpulan sampah otomatis minimal, dan GC memperkenalkan banyak masalah sendiri. Karena itu, saya menyarankan bahwa "Tidak banyak" adalah jawaban untuk masalah Anda.

Ingat, ketika orang berpikir tentang non-GC, mereka berpikir mallocdan free. Tapi ini adalah kesalahan logis yang besar - Anda akan membandingkan manajemen sumber daya non-GC pada awal 1970-an dengan pemulung di akhir tahun 90-an. Ini jelas perbandingan yang agak tidak adil - pengumpul sampah yang digunakan saat mallocdan freedirancang terlalu lambat untuk menjalankan program yang berarti, jika saya ingat dengan benar. Membandingkan sesuatu dari periode waktu yang samar-samar setara, misalnya unique_ptr, jauh lebih bermakna.

Pengumpul sampah dapat menangani siklus referensi dengan lebih mudah, meskipun ini adalah pengalaman yang sangat langka. Selain itu, GC hanya dapat "membuang" kode karena GC akan menangani semua manajemen memori, yang berarti bahwa mereka dapat menyebabkan siklus dev yang lebih cepat.

Di sisi lain, mereka cenderung mengalami masalah besar ketika berhadapan dengan memori yang datang dari mana pun kecuali kumpulan GC mereka sendiri. Selain itu, mereka kehilangan banyak manfaat ketika konkurensi terlibat, karena Anda tetap harus mempertimbangkan kepemilikan objek.

Sunting: Banyak hal yang Anda sebutkan tidak ada hubungannya dengan GC. Anda membingungkan manajemen memori dan orientasi objek. Lihat, ini masalahnya: Jika Anda memprogram dalam sistem yang tidak dikelola dengan tuntas, seperti C ++, Anda dapat memeriksa batas sebanyak yang Anda inginkan, dan kelas kontainer standar memang menawarkannya. Tidak ada GC tentang batas memeriksa, misalnya, atau pengetikan yang kuat.

Masalah yang Anda sebutkan diselesaikan dengan orientasi objek, bukan GC. Asal usul memori array dan memastikan Anda tidak menulis di luarnya adalah konsep ortogonal.

Sunting: Perlu dicatat bahwa teknik yang lebih maju dapat menghindari kebutuhan akan segala bentuk alokasi memori dinamis sama sekali. Misalnya, pertimbangkan penggunaan ini , yang mengimplementasikan kombinasi Y di C ++ tanpa alokasi dinamis sama sekali.

DeadMG
sumber
Diskusi yang diperpanjang di sini telah dibersihkan: jika semua orang dapat membawanya untuk mengobrol untuk membahas topik lebih lanjut, saya akan sangat menghargainya.
@DeadMG, tahukah Anda apa yang seharusnya dilakukan oleh kombinator? Seharusnya gABUNGAN. Menurut definisi, kombinator adalah fungsi tanpa variabel bebas.
SK-logic
2
@ SK-logic: Saya bisa memilih untuk mengimplementasikannya murni dengan templat dan tidak memiliki variabel anggota. Tapi kemudian Anda tidak akan bisa melakukan penutupan, yang secara signifikan membatasi kegunaannya. Mau datang untuk ngobrol?
DeadMG
@DeadMG, definisi jelas. Tidak ada variabel gratis. Saya menganggap bahasa apa pun "cukup fungsional" jika memungkinkan untuk mendefinisikan Y-combinator (dengan benar, bukan cara Anda). Tanda "+" yang besar adalah jika memungkinkan untuk mendefinisikannya melalui kombinasi S, K dan I. Kalau tidak, bahasa tidak cukup ekspresif.
SK-logic
4
@ SK-logic: Mengapa Anda tidak datang ke obrolan , seperti yang ditanyakan moderator? Juga, Y-combinator adalah Y-combinator, ia melakukan pekerjaan atau tidak. Versi Haskell dari Y-combinator pada dasarnya persis sama dengan yang ini, hanya saja keadaan yang diungkapkan tersembunyi dari Anda.
DeadMG
11

"Kebebasan dari keharusan untuk khawatir tentang membebaskan sumber" yang bahasa sampah-dikumpulkan seharusnya memberikan adalah untuk yang cukup luas ilusi. Terus menambahkan barang ke peta tanpa pernah menghapus, dan Anda akan segera mengerti apa yang saya bicarakan.

Bahkan, kebocoran memori cukup sering terjadi dalam program yang ditulis dalam bahasa GCed, karena bahasa ini cenderung membuat programmer malas, dan membuat mereka memperoleh rasa aman yang salah bahwa bahasa akan selalu entah bagaimana (secara ajaib) menjaga setiap objek yang mereka gunakan. tidak ingin harus memikirkan lagi.

Pengumpulan sampah hanyalah fasilitas yang diperlukan untuk bahasa yang memiliki tujuan lain yang lebih mulia: untuk memperlakukan segala sesuatu sebagai penunjuk ke objek, dan pada saat yang sama menyembunyikan dari pemrogram fakta bahwa itu adalah penunjuk, sehingga pemrogram tidak dapat melakukan bunuh diri dengan mencoba pointer aritmatika dan sejenisnya. Segala sesuatu yang menjadi objek berarti bahwa bahasa GCed perlu mengalokasikan objek jauh lebih sering daripada bahasa non-GCed, yang berarti bahwa jika mereka meletakkan beban deallocating objek pada pemrogram, mereka akan sangat tidak menarik.

Juga, pengumpulan sampah berguna untuk memberikan programmer kemampuan untuk menulis kode ketat, memanipulasi objek di dalam ekspresi, dengan cara pemrograman fungsional, tanpa harus memecah ekspresi menjadi pernyataan terpisah untuk menyediakan deallokasi setiap objek tunggal yang berpartisipasi dalam ekspresi.

Selain dari semua itu, silahkan perhatikan bahwa di awal jawaban saya saya menulis "itu adalah untuk yang cukup luas ilusi". Saya tidak menulis bahwa itu adalah ilusi. Saya bahkan tidak menulis bahwa itu kebanyakan hanya ilusi. Pengumpulan sampah berguna dalam mengambil dari programmer tugas kasar menghadiri ke penempatan objek-objeknya. Jadi, dalam hal ini ini adalah fitur produktivitas.

Mike Nakis
sumber
4

Pengumpul sampah tidak membahas "bug" apa pun. Ini adalah bagian penting dari beberapa semantik bahasa tingkat tinggi. Dengan GC adalah mungkin untuk mendefinisikan tingkat abstraksi yang lebih tinggi, seperti penutupan leksikal dan sejenisnya, sedangkan dengan manajemen memori manual abstraksi tersebut akan bocor, yang tidak perlu terikat pada tingkat yang lebih rendah dari manajemen sumber daya.

"Prinsip kepemilikan tunggal", yang disebutkan dalam komentar, adalah contoh yang sangat baik dari abstraksi yang bocor. Seorang pengembang tidak boleh khawatir sama sekali tentang jumlah tautan ke instance struktur data elementer tertentu, jika tidak, setiap bagian dari kode tidak akan generik dan transparan tanpa sejumlah besar pembatasan tambahan dan persyaratan (tidak langsung terlihat dalam kode itu sendiri) . Kode semacam itu tidak dapat disusun menjadi kode tingkat yang lebih tinggi, yang merupakan pelanggaran prinsip pemisahan tanggung jawab (blok pembangun utama rekayasa perangkat lunak, yang tidak dapat ditoleransi sama sekali oleh sebagian besar pengembang tingkat rendah).

Logika SK
sumber
1
@Mason Wheeler, bahkan C ++ mengimplementasikan bentuk penutupan yang sangat terbatas. Tetapi hampir tidak layak, umumnya dapat digunakan penutupan.
SK-logic
1
Anda salah. Tidak ada GC yang dapat melindungi Anda dari fakta bahwa Anda tidak dapat merujuk ke variabel tumpukan. Dan itu lucu- di C ++, Anda dapat mengambil "Salin pointer ke variabel yang dialokasikan secara dinamis yang akan tepat dan secara otomatis dihancurkan" pendekatan juga.
DeadMG
1
@DeadMG, tidakkah Anda melihat bahwa kode Anda membocorkan entitas tingkat rendah melalui level lain yang Anda bangun di atas?
SK-logic
1
@ SK-Logic: OK, kami memiliki masalah terminologi. Apa definisi Anda tentang "penutupan nyata," dan apa yang bisa mereka lakukan sehingga penutupan Delphi tidak bisa? (Dan termasuk segala sesuatu tentang manajemen memori dalam definisi Anda adalah memindahkan posting tujuan. Mari kita bicara tentang perilaku, bukan detail implementasi.)
Mason Wheeler
1
@ SK-Logic: ... dan apakah Anda memiliki contoh tentang sesuatu yang dapat dilakukan dengan penutupan lambda sederhana yang tidak diketik yang tidak dapat dicapai oleh penutupan Delphi?
Mason Wheeler
2

Sungguh, mengelola memori Anda sendiri hanyalah satu lagi sumber bug yang potensial.

Jika Anda lupa panggilan ke free(atau apa pun yang sederajat dalam bahasa apa pun yang Anda gunakan), program Anda dapat lulus semua tesnya, tetapi membocorkan memori. Dan dalam program yang cukup kompleks, cukup mudah untuk mengabaikan panggilan free.

Dawood berkata mengembalikan Monica
sumber
3
Ketinggalan freebukanlah hal terburuk. Awal freejauh lebih dahsyat.
herby
2
Dan ganda free!
quant_dev
Hehe! Saya akan setuju dengan kedua komentar di atas. Saya sendiri tidak pernah melakukan salah satu dari pelanggaran ini (sejauh yang saya tahu), tetapi saya dapat melihat seberapa buruk efeknya. Jawaban dari quant_dev mengatakan semuanya - kesalahan dengan alokasi memori dan de-alokasi sangat sulit ditemukan dan diperbaiki.
Dawood mengatakan mengembalikan Monica
1
Ini adalah kekeliruan. Anda membandingkan "awal 1970" hingga "akhir 1990". GC yang ada pada saat itu mallocdan freemerupakan cara non-GC untuk berjalan terlalu lambat untuk berguna untuk apa pun. Anda harus membandingkannya dengan pendekatan non-GC modern, seperti RAII.
DeadMG
2
@DeadMG RAII bukan manajemen memori manual
quant_dev
2

Sumber daya manual tidak hanya membosankan, tetapi juga sulit untuk di-debug. Dengan kata lain, tidak hanya membosankan untuk memperbaikinya, tetapi juga ketika Anda salah, tidak jelas di mana masalahnya. Ini karena, tidak seperti misalnya pembagian dengan nol, efek dari kesalahan muncul jauh dari sumber kesalahan, dan menghubungkan titik-titik membutuhkan waktu, perhatian dan pengalaman.

quant_dev
sumber
1

Saya pikir pengumpulan sampah mendapat banyak pujian untuk perbaikan bahasa yang tidak ada hubungannya dengan GC, selain menjadi bagian dari satu gelombang besar kemajuan.

Satu manfaat solid untuk GC yang saya tahu adalah bahwa Anda dapat mengatur objek gratis di program Anda dan tahu itu akan hilang ketika semua orang selesai dengan itu. Anda bisa meneruskannya ke metode kelas lain dan tidak khawatir tentang hal itu. Anda tidak peduli metode apa yang dilewatkan, atau kelas apa yang merujuknya. (Kebocoran memori adalah tanggung jawab kelas yang mereferensikan objek, bukan kelas yang membuatnya.)

Tanpa GC Anda harus melacak seluruh siklus memori yang dialokasikan. Setiap kali Anda melewati alamat ke atas atau ke bawah dari subrutin yang membuatnya, Anda memiliki referensi di luar kendali untuk memori itu. Di masa lalu yang buruk, bahkan dengan hanya satu utas, rekursi, dan sistem operasi yang rumit (Windows NT) membuat saya tidak mungkin mengontrol akses ke memori yang dialokasikan. Saya harus rig metode gratis di sistem alokasi saya sendiri untuk menjaga blok memori sekitar untuk sementara waktu sampai semua referensi dihapus. Waktu memegang itu murni dugaan, tapi itu berhasil.

Jadi itulah satu-satunya manfaat GC yang saya tahu, tetapi saya tidak bisa hidup tanpanya. Saya tidak berpikir OOP jenis apa pun akan terbang tanpanya.

RalphChapin
sumber
1
Tepat di atas kepala saya, Delphi dan C ++ keduanya cukup sukses sebagai bahasa OOP tanpa GC. Yang Anda butuhkan untuk mencegah "referensi tidak terkendali" adalah sedikit disiplin. Jika Anda memahami Prinsip Kepemilikan Tunggal, (lihat jawaban saya,) masalah yang Anda bicarakan di sini menjadi sama sekali bukan masalah.
Mason Wheeler
@MasonWheeler: Ketika tiba saatnya objek pemilik dibebaskan, ia perlu mengetahui semua tempat yang dirujuk objeknya. Mempertahankan informasi ini dan menggunakannya untuk menghapus referensi tampak seperti pekerjaan yang mengerikan bagi saya. Saya sering menemukan referensi belum bisa dihapus. Saya harus menandai pemilik sebagai terhapus, lalu menghidupkannya secara berkala untuk melihat apakah pemiliknya dapat membebaskan dirinya dengan aman. Saya tidak pernah menggunakan Delphi, tetapi untuk pengorbanan kecil dalam efisiensi pelaksanaan C # / Java memberi saya dorongan besar dalam waktu pengembangan lebih dari C ++. (Tidak semua karena GC, tapi itu membantu.)
RalphChapin
1

Kebocoran Fisik

Jenis bug yang ditangani oleh GC tampaknya (setidaknya bagi pengamat eksternal) hal-hal yang tidak dapat dilakukan oleh programmer yang mengerti bahasa, perpustakaan, konsep, idiom, dll. Tapi saya bisa saja salah: apakah penanganan memori manual pada hakekatnya rumit?

Berasal dari ujung C yang membuat manajemen memori sekitar sebagai manual dan diucapkan mungkin sehingga kami membandingkan ekstrem (C ++ sebagian besar mengotomatiskan manajemen memori tanpa GC), saya akan mengatakan "tidak benar-benar" dalam arti membandingkan dengan GC ketika datang ke kebocoran . Seorang pemula dan kadang-kadang bahkan seorang profesional mungkin lupa menulis freeuntuk diberikan malloc. Ini pasti terjadi.

Namun, ada alat seperti valgrinddeteksi kebocoran yang akan segera ditemukan, pada saat mengeksekusi kode, ketika / di mana kesalahan tersebut terjadi hingga ke baris kode yang tepat. Ketika itu diintegrasikan ke dalam CI, menjadi hampir tidak mungkin untuk menggabungkan kesalahan seperti itu, dan mudah untuk memperbaikinya. Jadi itu tidak pernah menjadi masalah besar dalam tim / proses apa pun dengan standar yang masuk akal.

Memang, mungkin ada beberapa kasus eksotis eksekusi yang terbang di bawah radar pengujian di mana freegagal dipanggil, mungkin saat menghadapi kesalahan input eksternal yang tidak jelas seperti file korup di mana kasus mungkin sistem bocor 32 byte atau sesuatu. Saya pikir itu pasti dapat terjadi bahkan di bawah standar pengujian yang cukup bagus dan alat deteksi kebocoran, tetapi juga tidak terlalu kritis untuk membocorkan sedikit memori pada sesuatu yang hampir tidak pernah terjadi. Kita akan melihat masalah yang jauh lebih besar di mana kita dapat membocorkan sumber daya besar bahkan di jalur eksekusi umum di bawah dengan cara yang tidak dapat dicegah oleh GC.

Ini juga sulit tanpa sesuatu yang menyerupai pseudo-bentuk GC (penghitungan referensi, misalnya) ketika masa hidup suatu objek perlu diperpanjang untuk beberapa bentuk pemrosesan yang ditangguhkan / tidak sinkron, mungkin dengan utas lainnya.

Pointer Menggantung

Masalah sebenarnya dengan bentuk manajemen memori yang lebih manual tidak bocor bagi saya. Berapa banyak aplikasi asli yang ditulis dalam C atau C ++ yang kita ketahui benar-benar bocor? Apakah kernel Linux bocor? MySQL? CryEngine 3? Stasiun kerja dan synthesizer audio digital? Apakah Java VM bocor (ini diterapkan dalam kode asli)? Photoshop?

Jika ada, saya pikir ketika kita melihat-lihat, aplikasi yang paling sedikit cenderung yang ditulis menggunakan skema GC. Tetapi sebelum itu dianggap sebagai slam pada pengumpulan sampah, kode asli memiliki masalah signifikan yang sama sekali tidak terkait dengan kebocoran memori.

Masalah bagi saya selalu aman. Bahkan ketika kita freemengingat melalui pointer, jika ada pointer lain ke sumber daya, mereka akan menjadi pointer (tidak valid) menggantung.

Ketika kami mencoba mengakses pointees dari pointer yang menggantung itu, kami akhirnya menemukan perilaku yang tidak terdefinisi, meskipun hampir selalu merupakan pelanggaran segfault / akses yang mengarah pada crash yang keras dan langsung.

Semua aplikasi asli yang saya sebutkan di atas berpotensi memiliki satu atau dua kasus tepi yang tidak jelas yang dapat menyebabkan crash terutama karena masalah ini, dan pasti ada bagian yang adil dari aplikasi jelek yang ditulis dalam kode asli yang sangat crash-heavy, dan seringkali sebagian besar karena masalah ini.

... dan itu karena manajemen sumber daya sulit terlepas dari apakah Anda menggunakan GC atau tidak. Perbedaan praktisnya seringkali bocor (GC) atau menabrak (tanpa GC) dalam menghadapi kesalahan yang mengarah pada kesalahan pengelolaan sumber daya.

Manajemen Sumber Daya: Pengumpulan Sampah

Manajemen sumber daya yang kompleks adalah proses manual yang sulit, apa pun yang terjadi. GC tidak dapat mengotomatiskan apa pun di sini.

Mari kita ambil contoh di mana kita memiliki objek ini, "Joe". Joe direferensikan oleh sejumlah organisasi yang menjadi anggotanya. Setiap bulan atau lebih mereka mengambil biaya keanggotaan dari kartu kreditnya.

masukkan deskripsi gambar di sini

Kami juga memiliki satu referensi untuk Joe untuk mengendalikan hidupnya. Katakanlah, sebagai programmer, kita tidak lagi membutuhkan Joe. Dia mulai mengganggu kita dan kita tidak lagi membutuhkan organisasi yang menjadi miliknya untuk membuang waktu berurusan dengannya. Jadi kami berusaha untuk menghapusnya dari muka bumi dengan menghapus referensi garis hidupnya.

masukkan deskripsi gambar di sini

... tapi tunggu, kami menggunakan pengumpulan sampah. Setiap referensi kuat untuk Joe akan membuatnya tetap ada. Jadi kami juga menghapus referensi kepadanya dari organisasi tempat dia berada (berhenti berlangganan).

masukkan deskripsi gambar di sini

... kecuali whoops, kami lupa membatalkan langganan majalahnya! Sekarang Joe tetap ada di memori, mengganggu kami dan menghabiskan sumber daya, dan perusahaan majalah juga akhirnya terus memproses keanggotaan Joe setiap bulan.

Ini adalah kesalahan utama yang dapat menyebabkan banyak program rumit yang ditulis menggunakan skema pengumpulan sampah bocor dan mulai menggunakan semakin banyak memori semakin lama berjalan, dan mungkin semakin banyak pemrosesan (berlangganan majalah berulang). Mereka lupa untuk menghapus satu atau lebih referensi itu, sehingga mustahil bagi pengumpul sampah untuk melakukan keajaiban sampai seluruh program ditutup.

Namun, program ini tidak macet. Sangat aman. Ini hanya akan terus memonopoli memori dan Joe masih akan berlama-lama. Untuk banyak aplikasi, perilaku bocor semacam ini di mana kita hanya membuang lebih banyak memori / pemrosesan pada masalah ini mungkin jauh lebih baik daripada hard crash, terutama mengingat berapa banyak memori dan daya pemrosesan yang dimiliki mesin kita saat ini.

Manajemen Sumber Daya: Manual

Sekarang mari kita pertimbangkan alternatif di mana kita menggunakan pointer ke Joe dan manajemen memori manual, seperti:

masukkan deskripsi gambar di sini

Tautan biru ini tidak mengatur masa pakai Joe. Jika kami ingin menghapusnya dari muka bumi, kami secara manual meminta untuk menghancurkannya, seperti:

masukkan deskripsi gambar di sini

Sekarang biasanya akan meninggalkan kita dengan pointer menggantung di semua tempat, jadi mari kita hapus pointer ke Joe.

masukkan deskripsi gambar di sini

... wah, kami membuat kesalahan yang sama lagi dan lupa untuk berhenti berlangganan majalah Joe!

Kecuali sekarang kita memiliki pointer menggantung. Ketika langganan majalah mencoba memproses biaya bulanan Joe, seluruh dunia akan meledak - biasanya kita mendapatkan hard crash langsung.

Kesalahan manajemen sumber daya dasar yang sama di mana pengembang lupa untuk menghapus secara manual semua petunjuk / referensi ke sumber daya dapat menyebabkan banyak crash pada aplikasi asli. Mereka tidak menyimpan memori lebih lama mereka berjalan biasanya karena mereka akan sering crash dalam kasus ini.

Dunia nyata

Sekarang contoh di atas menggunakan diagram yang sangat sederhana. Aplikasi dunia nyata mungkin memerlukan ribuan gambar dijahit bersama untuk menutupi grafik penuh, dengan ratusan jenis sumber daya yang berbeda disimpan dalam grafik adegan, sumber daya GPU yang terkait dengan beberapa dari mereka, akselerator terikat dengan yang lain, pengamat yang didistribusikan di ratusan plugin menonton sejumlah jenis entitas dalam adegan untuk perubahan, pengamat yang mengamati pengamat, audio yang disinkronkan dengan animasi, dll. Jadi, sepertinya mudah untuk menghindari kesalahan yang saya jelaskan di atas, tetapi umumnya tidak ada yang dekat dengan hal sederhana ini di dunia nyata basis kode produksi untuk aplikasi kompleks yang menjangkau jutaan baris kode.

Kemungkinan seseorang, suatu hari, akan salah mengelola sumber daya di suatu tempat dalam basis kode itu cenderung cukup tinggi, dan probabilitasnya sama dengan atau tanpa GC. Perbedaan utama adalah apa yang akan terjadi sebagai akibat dari kesalahan ini, yang juga mempengaruhi secara potensial mempengaruhi seberapa cepat kesalahan ini akan terlihat dan diperbaiki.

Kecelakaan vs. Kebocoran

Sekarang yang mana yang lebih buruk? Kecelakaan seketika, atau keheningan diam-diam bocor di mana Joe secara misterius hidup?

Sebagian besar mungkin menjawab yang terakhir, tetapi katakanlah perangkat lunak ini dirancang untuk dijalankan selama berjam-jam, mungkin berhari-hari, dan masing-masing Joe dan Jane yang kami tambahkan meningkatkan penggunaan memori perangkat lunak sebesar satu gigabyte. Ini bukan perangkat lunak mission-critical (crash tidak benar-benar membunuh pengguna), tetapi kritis-kinerja.

Dalam hal ini, crash keras yang segera muncul saat debugging, menunjukkan kesalahan yang Anda buat, mungkin sebenarnya lebih baik daripada hanya perangkat lunak bocor yang bahkan mungkin terbang di bawah radar prosedur pengujian Anda.

Di sisi lain, jika itu adalah perangkat lunak misi-kritis di mana kinerja bukan tujuan, hanya saja tidak menabrak dengan cara apa pun yang mungkin, maka bocor mungkin sebenarnya lebih disukai.

Referensi yang lemah

Ada semacam hibrida dari ide-ide ini yang tersedia dalam skema GC yang dikenal sebagai referensi lemah. Dengan referensi yang lemah, kita dapat memiliki semua organisasi ini sebagai referensi lemah Joe tetapi tidak mencegahnya dihapus ketika referensi kuat (pemilik / garis hidup Joe) hilang. Namun demikian, kita mendapat manfaat dari dapat mendeteksi ketika Joe tidak lagi melalui referensi yang lemah ini, memungkinkan kita untuk mendapatkan jenis kesalahan yang mudah direproduksi.

Sayangnya referensi yang lemah tidak digunakan sebanyak yang seharusnya digunakan, jadi sering kali banyak aplikasi GC kompleks rentan terhadap kebocoran bahkan jika mereka berpotensi jauh lebih tidak crash daripada aplikasi C kompleks, misalnya

Bagaimanapun, apakah GC membuat hidup Anda lebih mudah atau lebih sulit tergantung pada seberapa penting bagi perangkat lunak Anda untuk menghindari kebocoran, dan apakah itu berhubungan dengan manajemen sumber daya yang kompleks atau tidak.

Dalam kasus saya, saya bekerja di bidang kritis kinerja di mana sumber daya melakukan rentang ratusan megabyte hingga gigabytes, dan tidak melepaskan memori itu ketika pengguna meminta untuk membongkar karena kesalahan seperti di atas sebenarnya bisa lebih disukai daripada crash. Gangguan mudah dikenali dan direproduksi, membuatnya sering menjadi bug favorit programmer, bahkan jika itu adalah bug yang paling tidak disukai pengguna, dan banyak dari crash ini akan muncul dengan prosedur pengujian yang waras bahkan sebelum mereka mencapai pengguna.

Bagaimanapun, itulah perbedaan antara GC dan manajemen memori manual. Untuk menjawab pertanyaan langsung Anda, saya akan mengatakan manajemen memori manual sulit, tetapi tidak ada hubungannya dengan kebocoran, dan baik GC maupun bentuk manajemen memori manual masih sangat sulit ketika manajemen sumber daya tidak sepele. GC bisa dibilang memiliki perilaku yang lebih rumit di sini di mana program tersebut tampaknya berfungsi dengan baik tetapi memakan sumber daya yang semakin banyak. Bentuk manual kurang rumit, tetapi akan crash dan membakar waktu besar dengan kesalahan seperti yang ditunjukkan di atas.


sumber
-1

Berikut adalah daftar masalah yang dihadapi oleh programmer C ++ ketika berhadapan dengan memori:

  1. Masalah pelingkupan terjadi dalam memori yang dialokasikan tumpukan: itu seumur hidup tidak melampaui di luar fungsi di mana itu dialokasikan masuk. Ada tiga solusi utama untuk masalah ini: memori tumpukan, dan memindahkan titik alokasi ke atas dalam panggilan stack atau mengalokasikan dari objek di dalam .
  2. Masalah sizeof adalah di stack dialokasikan dan mengalokasikan dari objek di dalam dan sebagian memori tumpukan dialokasikan: Ukuran blok memori tidak dapat berubah pada saat runtime. Solusi adalah tumpukan memori array, pointer dan perpustakaan dan wadah.
  3. Urutan masalah definisi adalah ketika mengalokasikan dari objek di dalam: kelas di dalam program harus dalam urutan yang benar. Solusi membatasi dependensi ke pohon dan menyusun ulang kelas dan tidak menggunakan deklarasi maju, dan pointer dan menumpuk memori dan menggunakan deklarasi maju.
  4. Masalah dalam-luar adalah dalam memori yang dialokasikan objek. Akses memori di dalam objek dibagi menjadi dua bagian, sebagian memori ada di dalam objek dan memori lain di luarnya, dan programmer harus memilih dengan benar untuk menggunakan komposisi atau referensi berdasarkan keputusan ini. Solusi melakukan keputusan dengan benar, atau pointer dan menumpuk memori.
  5. Masalah objek rekursif adalah dalam memori yang dialokasikan objek. Ukuran objek menjadi tak terbatas jika objek yang sama ditempatkan di dalam dirinya sendiri, dan solusinya adalah referensi, memori tumpukan dan pointer.
  6. Masalah pelacakan kepemilikan ada dalam memori yang dialokasikan heap, pointer yang berisi alamat memori yang dialokasikan heap harus diteruskan dari titik alokasi ke titik deallokasi. Solusi adalah memori yang dialokasikan stack, memori yang dialokasikan objek, auto_ptr, shared_ptr, unique_ptr, wadah stdlib.
  7. Masalah duplikasi kepemilikan ada di memori yang dialokasikan tumpukan: deallokasi hanya dapat dilakukan sekali. Solusi adalah memori yang dialokasikan stack, memori yang dialokasikan objek, auto_ptr, shared_ptr, unique_ptr, wadah stdlib.
  8. Masalah Null pointer ada dalam memori yang dialokasikan heap: pointer diperbolehkan menjadi NULL yang membuat banyak operasi macet saat runtime. Solusinya adalah memori tumpukan, memori yang dialokasikan objek dan analisis cermat area tumpukan dan referensi.
  9. Masalah kebocoran memori ada dalam memori yang dialokasikan tumpukan: Lupa memanggil hapus untuk setiap blok memori yang dialokasikan. Solusi adalah alat seperti valgrind.
  10. Masalah stack overflow adalah untuk panggilan fungsi rekursif yang menggunakan memori tumpukan. Biasanya ukuran tumpukan sepenuhnya ditentukan pada waktu kompilasi, kecuali untuk kasus algoritma rekursif. Mendefinisikan ukuran stack OS salah juga sering menyebabkan masalah ini karena tidak ada cara untuk mengukur ukuran ruang stack yang diperlukan.

Seperti yang Anda lihat, heap memory menyelesaikan sangat banyak masalah yang ada, tetapi menyebabkan kompleksitas tambahan. GC dirancang untuk menangani bagian dari kompleksitas itu. (maaf jika beberapa nama masalah bukan nama yang tepat untuk masalah ini - terkadang sulit untuk mengetahui nama yang benar)

tp1
sumber
1
-1: Bukan jawaban untuk pertanyaan itu.
Sjoerd