Mengapa pointer pintar penghitungan referensi sangat populer?

52

Seperti yang saya lihat, smart pointer digunakan secara luas di banyak proyek C ++ dunia nyata.

Meskipun beberapa jenis pointer cerdas jelas bermanfaat untuk mendukung RAII dan transfer kepemilikan, ada juga kecenderungan menggunakan pointer bersama secara default , sebagai cara "pengumpulan sampah" , sehingga pemrogram tidak harus memikirkan alokasi sebanyak itu. .

Mengapa pointer bersama lebih populer daripada mengintegrasikan pengumpul sampah yang tepat seperti Boehm GC ? (Atau apakah Anda setuju sama sekali, bahwa mereka lebih populer daripada GC yang sebenarnya?)

Saya tahu dua keunggulan GC konvensional dibandingkan penghitungan referensi:

  • Algoritma GC konvensional tidak memiliki masalah dengan siklus referensi .
  • Referensi-hitungan umumnya lebih lambat dari GC yang tepat.

Apa alasan untuk menggunakan smart pointer penghitungan referensi?

Miklós Homolya
sumber
6
Saya baru saja menambahkan komentar bahwa ini adalah default yang salah untuk digunakan: dalam kebanyakan kasus, std::unique_ptrsudah cukup dan karena itu memiliki overhead nol atas pointer mentah dalam hal kinerja run-time. Dengan menggunakan di std::shared_ptrmana - mana Anda juga akan mengaburkan semantik kepemilikan, kehilangan salah satu manfaat utama dari pointer pintar selain manajemen sumber daya otomatis - pemahaman yang jelas tentang maksud di balik kode.
Matt
2
Maaf tapi jawaban yang diterima di sini benar-benar salah. Penghitungan referensi memiliki overhead yang lebih tinggi (hitungan alih-alih nilai bit dan kinerja run-time yang lebih lambat), waktu jeda yang tidak terbatas ketika mengurangi longsoran salju dan tidak ada yang lebih rumit, katakanlah, Cheney semi-space.
Jon Harrop

Jawaban:

57

Beberapa keuntungan penghitungan referensi dibandingkan pengumpulan sampah:

  1. Overhead rendah. Pengumpul sampah bisa sangat mengganggu (mis. Membuat program Anda macet di waktu yang tidak dapat diprediksi saat siklus pengumpulan sampah) dan cukup intensif terhadap memori (mis. Jejak memori proses Anda yang tidak perlu tumbuh hingga beberapa megabyte sebelum pengumpulan sampah akhirnya dimulai)

  2. Perilaku yang lebih mudah ditebak. Dengan penghitungan referensi, Anda dijamin bahwa objek Anda akan dibebaskan saat referensi terakhir hilang. Dengan pengumpulan sampah, di sisi lain, objek Anda akan dibebaskan "kapan", ketika sistem berhasil melakukannya. Untuk RAM ini biasanya bukan masalah besar pada desktop atau server yang sedikit dimuat, tetapi untuk sumber daya lain (mis. File menangani) Anda sering membutuhkan mereka ditutup SECEPATNYA untuk menghindari potensi konflik di kemudian hari.

  3. Lebih sederhana. Penghitungan referensi dapat dijelaskan dalam beberapa menit, dan diterapkan dalam satu atau dua jam. Pengumpul sampah, terutama yang memiliki kinerja baik, sangat kompleks dan tidak banyak orang yang memahaminya.

  4. Standar. C ++ termasuk penghitungan referensi (melalui shared_ptr) dan teman-teman di STL, yang berarti bahwa sebagian besar programmer C ++ sudah mengenalnya dan sebagian besar kode C ++ akan bekerja dengannya. Namun, tidak ada pengumpul sampah C ++ standar, yang artinya Anda harus memilih satu dan berharap itu berfungsi dengan baik untuk kasus penggunaan Anda - dan jika tidak, itu masalah Anda untuk diperbaiki, bukan bahasa.

Adapun dugaan kerugian penghitungan referensi - tidak mendeteksi siklus adalah masalah, tapi salah satu yang saya tidak pernah bertemu secara pribadi dalam sepuluh tahun terakhir menggunakan penghitungan referensi. Sebagian besar struktur data bersifat asiklikal alami, dan jika Anda menemukan situasi di mana Anda memerlukan referensi siklikal (mis. Pointer orangtua di simpul pohon), Anda bisa menggunakan pelemahan_ptr atau pointer C mentah untuk "arah mundur". Selama Anda mengetahui masalah potensial saat Anda merancang struktur data Anda, itu bukan masalah.

Adapun kinerja, saya tidak pernah punya masalah dengan kinerja penghitungan referensi. Saya memiliki masalah dengan kinerja pengumpulan sampah, khususnya pembekuan acak yang dapat dilakukan oleh GC, di mana satu-satunya solusi ("jangan mengalokasikan objek") mungkin juga diulangi dengan "jangan gunakan GC" .

Jeremy Friesner
sumber
16
Implementasi penghitungan referensi yang naif biasanya mendapatkan throughput yang jauh lebih rendah daripada GC produksi (30-40%) dengan mengorbankan latensi. Kesenjangan dapat ditutup dengan optimisasi seperti menggunakan bit lebih sedikit untuk refcount, dan menghindari pelacakan objek sampai melarikan diri — C ++ melakukan ini secara alami jika Anda terutama make_sharedketika kembali. Meski demikian, latensi cenderung menjadi masalah yang lebih besar dalam aplikasi waktu nyata, tetapi throughput lebih penting secara umum, itulah sebabnya pelacakan GC sangat banyak digunakan. Saya tidak akan begitu cepat untuk berbicara buruk tentang mereka.
Jon Purdy
3
Saya akan berdalih 'lebih sederhana': lebih sederhana dalam hal jumlah total implementasi yang diperlukan ya, tetapi tidak lebih sederhana untuk kode yang menggunakannya : bandingkan dengan memberi tahu seseorang cara menggunakan RC ('lakukan ini saat membuat objek dan ini saat menghancurkannya' ) untuk cara (secara naif, yang cukup sering) menggunakan GC ('...').
AakashM
4
"Dengan penghitungan referensi, Anda dijamin bahwa objek Anda akan dibebaskan saat referensi terakhir hilang". Itu adalah kesalahpahaman umum. flyingfrogblog.blogspot.co.uk/2013/10/...
Jon Harrop
4
@ JonHarrop: Posting blog itu benar-benar salah arah. Anda juga harus membaca semua komentar, terutama yang terakhir.
Deduplicator
3
@ JonHarrop: Ya, ada. Dia tidak mengerti bahwa masa hidup adalah cakupan penuh yang naik ke penjepit penutup. Dan optimasi di F # yang menurut komentar hanya kadang-kadang berfungsi mengakhiri masa hidup sebelumnya, jika variabel tidak digunakan lagi. Yang secara alami memiliki bahaya sendiri.
Deduplicator
26

Untuk mendapatkan kinerja yang baik dari GC, GC harus dapat memindahkan objek dalam memori. Dalam bahasa seperti C ++ di mana Anda dapat berinteraksi langsung dengan lokasi memori, ini sangat mustahil. (Microsoft C ++ / CLR tidak masuk hitungan karena memperkenalkan sintaks baru untuk pointer yang dikelola GC dan karenanya secara efektif merupakan bahasa yang berbeda.)

Boehm GC, meskipun ide yang bagus, sebenarnya adalah yang terburuk dari kedua dunia: Anda membutuhkan malloc () yang lebih lambat daripada GC yang baik, dan karenanya Anda kehilangan perilaku alokasi / deallokasi yang deterministik tanpa peningkatan kinerja yang sesuai dari GC generasi . Ditambah lagi dengan keharusan konservatif, jadi tidak harus mengumpulkan semua sampah Anda.

GC yang baik dan selaras bisa menjadi hal yang hebat. Tetapi dalam bahasa seperti C ++, keuntungannya minimal dan biayanya sering tidak sepadan.

Akan menarik untuk melihat, bagaimanapun, ketika C ++ 11 menjadi lebih populer, apakah lambda dan semantik tangkap mulai memimpin komunitas C ++ ke arah masalah alokasi dan objek seumur hidup yang sama yang menyebabkan komunitas Lisp menciptakan GCs pada awalnya tempat.

Lihat juga jawaban saya untuk pertanyaan terkait di StackOverflow .

Daniel Pryden
sumber
6
RE the Boehm GC, saya kadang-kadang bertanya-tanya berapa banyak secara pribadi bertanggung jawab atas keengganan tradisional untuk GC di antara programmer C dan C ++ hanya dengan memberikan kesan pertama yang buruk dari teknologi secara umum.
Leushenko
@Leushenko Nah berkata. Contoh kasusnya adalah pertanyaan ini, di mana Boehm gc disebut "layak" gc, mengabaikan fakta bahwa itu lambat dan praktis dijamin bocor. Saya menemukan pertanyaan ini ketika meneliti apakah seseorang menerapkan pemutus siklus gaya-python untuk shared_ptr, yang kedengarannya seperti tujuan yang berharga untuk implementasi c ++.
user4815162342
4

Seperti yang saya lihat, smart pointer digunakan secara luas di banyak proyek C ++ dunia nyata.

Benar tetapi, secara objektif, sebagian besar kode sekarang ditulis dalam bahasa modern dengan melacak pengumpul sampah.

Meskipun beberapa jenis pointer cerdas jelas bermanfaat untuk mendukung RAII dan transfer kepemilikan, ada juga kecenderungan menggunakan pointer bersama secara default, sebagai cara "pengumpulan sampah", sehingga pemrogram tidak harus memikirkan alokasi sebanyak itu. .

Itu ide yang buruk karena Anda masih perlu khawatir tentang siklus.

Mengapa pointer bersama lebih populer daripada mengintegrasikan pengumpul sampah yang tepat seperti Boehm GC? (Atau apakah Anda setuju sama sekali, bahwa mereka lebih populer daripada GC yang sebenarnya?)

Oh wow, ada banyak hal yang salah dengan garis pemikiran Anda:

  1. Boehm's GC bukanlah GC yang "tepat" dalam arti kata apa pun. Benar-benar mengerikan. Itu konservatif sehingga bocor dan tidak efisien dengan desain. Lihat: http://flyingfrogblog.blogspot.co.uk/search/label/boehm

  2. Pointer bersama, secara objektif, jauh dari tempat yang sama populernya dengan GC karena sebagian besar pengembang menggunakan bahasa GC'd sekarang dan tidak memerlukan pointer bersama. Lihat saja Java dan Javascript di pasar kerja dibandingkan dengan C ++.

  3. Anda tampaknya membatasi pertimbangan untuk C ++ karena, saya berasumsi, Anda berpikir bahwa GC adalah masalah tangensial. Ini bukan ( satu - satunya cara untuk mendapatkan GC yang layak adalah merancang bahasa dan VM untuk itu sejak awal) sehingga Anda memperkenalkan bias seleksi. Orang-orang yang benar-benar menginginkan pengumpulan sampah yang tepat tidak bertahan dengan C ++.

Apa alasan untuk menggunakan smart pointer penghitungan referensi?

Anda dibatasi untuk C ++ tetapi berharap Anda memiliki manajemen memori otomatis.

Jon Harrop
sumber
7
Um, ini adalah pertanyaan yang ditandai c ++ yang berbicara tentang fitur C ++. Jelas, pernyataan umum apa pun yang dibicarakan dalam kode C ++, bukan keseluruhan pemrograman. Jadi bagaimanapun, "secara objektif" pengumpulan sampah mungkin digunakan di luar dunia C ++, yang pada akhirnya tidak relevan dengan pertanyaan yang ada.
Nicol Bolas
2
Baris terakhir Anda jelas-jelas salah: Anda berada di C ++ dan senang Anda tidak dipaksa berurusan dengan GC dan itu tertunda membebaskan sumber daya. Ada alasan mengapa Apple tidak menyukai GC, dan pedoman paling penting untuk bahasa-bahasa GC adalah: Jangan membuat sampah kecuali Anda memiliki sekumpulan sumber daya menganggur atau tidak dapat menahannya.
Deduplicator
3
@ JonHarrop: Jadi, bandingkan program kecil yang setara dengan dan tanpa GC, yang tidak dipilih secara eksplisit untuk bermain demi keuntungan kedua belah pihak. Mana yang Anda harapkan membutuhkan lebih banyak memori?
Deduplicator
1
@Deduplicator: Saya bisa membayangkan program yang memberikan hasil baik. Penghitungan referensi akan mengungguli pelacakan GC ketika program dirancang untuk menjaga tumpukan mengalokasikan memori sampai ia selamat dari pembibitan (misalnya antrian daftar) karena itu adalah kinerja patologis untuk GC generasi dan akan menghasilkan sampah terapung paling banyak. Menelusuri pengumpulan sampah akan membutuhkan lebih sedikit memori daripada penghitungan referensi berbasis lingkup ketika ada banyak objek kecil dan masa hidup yang singkat tetapi tidak dikenal secara statis sehingga sesuatu seperti program logika menggunakan struktur data murni fungsional.
Jon Harrop
3
@ JonHarrop: Maksud saya dengan GC (tracing atau apa pun) dan RAII jika Anda berbicara C ++. Yang termasuk penghitungan referensi, tetapi hanya di mana itu berguna. Atau Anda dapat membandingkan dengan program Swift.
Deduplicator
3

Di MacOS X dan iOS, dan dengan pengembang yang menggunakan Objective-C atau Swift, penghitungan referensi populer karena ditangani secara otomatis, dan penggunaan pengumpulan sampah sangat menurun karena Apple tidak mendukungnya lagi (saya diberi tahu bahwa aplikasi menggunakan pengumpulan sampah akan pecah di versi MacOS X berikutnya, dan pengumpulan sampah tidak pernah diterapkan di iOS). Sebenarnya saya benar-benar ragu bahwa ada banyak perangkat lunak yang menggunakan pengumpulan sampah ketika tersedia.

Alasan membuang pengumpulan sampah: Itu tidak pernah bekerja dengan baik di lingkungan bergaya C di mana pointer bisa "melarikan diri" ke daerah-daerah yang tidak dapat diakses oleh pengumpul sampah. Apple sangat percaya dan percaya bahwa penghitungan referensi lebih cepat. (Anda dapat membuat klaim apa pun di sini tentang kecepatan relatif, tetapi tidak ada yang dapat meyakinkan Apple). Dan pada akhirnya, tidak ada yang menggunakan pengumpulan sampah.

Hal pertama yang dipelajari pengembang MacOS X atau iOS adalah bagaimana menangani siklus referensi, jadi itu bukan masalah bagi pengembang nyata.

gnasher729
sumber
Cara saya memahaminya, itu bukan lingkungan seperti-C yang memutuskan sesuatu tetapi bahwa GC tidak pasti dan membutuhkan lebih banyak memori untuk memiliki kinerja yang dapat diterima, dan di luar server / desktop yang selalu sedikit langka.
Deduplicator
Membahas mengapa pemungut sampah menghancurkan sebuah benda yang masih saya gunakan (menyebabkan kecelakaan) memutuskannya untuk saya :-)
gnasher729
Oh ya, itu akan melakukannya juga. Apakah Anda pada akhirnya mencari tahu mengapa?
Deduplicator
Ya, itu adalah salah satu dari banyak fungsi Unix tempat Anda melewatkan kekosongan * sebagai "konteks" yang kemudian diberikan kembali kepada Anda dalam fungsi panggilan balik; kekosongan * benar-benar objek Objective-C, dan pengumpul sampah tidak menyadari bahwa objek tersebut disimpan dalam panggilan Unix. Panggilan balik disebut, melemparkan void * ke Object *, kaboom!
gnasher729
2

Kerugian terbesar dari pengumpulan sampah di C ++ adalah, sangat tidak mungkin untuk memperbaikinya:

  • Dalam C ++, pointer tidak hidup di komunitas mereka sendiri, mereka dicampur dengan data lain. Dengan demikian, Anda tidak dapat membedakan pointer dari data lain yang kebetulan memiliki pola bit yang dapat diartikan sebagai pointer yang valid.

    Konsekuensi: Setiap pengumpul sampah C ++ akan membocorkan objek yang harus dikumpulkan.

  • Di C ++, Anda bisa melakukan pointer aritmatika untuk mendapatkan pointer. Dengan demikian, jika Anda tidak menemukan pointer ke awal blok, itu tidak berarti bahwa blok itu tidak dapat dirujuk.

    Konsekuensi: Setiap pengumpul sampah C ++ harus memperhitungkan penyesuaian ini, memperlakukan setiap urutan bit yang terjadi ke titik mana pun dalam blok, termasuk tepat setelah akhir, sebagai penunjuk yang valid yang merujuk pada blok.

    Catatan: Tidak ada pengumpul sampah C ++ yang dapat menangani kode dengan trik seperti ini:

    int* array = new int[7];
    array--;    //undefined behavior, but people may be tempted anyway...
    for(int i = 1; i <= 7; i++) array[i] = i;
    

    Benar, ini memunculkan perilaku yang tidak terdefinisi. Tetapi beberapa kode yang ada lebih pintar dari pada yang baik untuk itu, dan mungkin memicu pemindahan lokasi awal oleh seorang pemulung.

cmaster
sumber
2
" Mereka dicampur dengan data lain. " Ini tidak terlalu banyak sehingga mereka "dicampur" dengan data lain. Sangat mudah untuk menggunakan sistem tipe C ++ untuk melihat apa itu pointer dan apa yang bukan. Masalahnya adalah bahwa pointer sering menjadi data lain. Menyembunyikan pointer di integer adalah alat yang sayangnya umum untuk banyak API C-style.
Nicol Bolas
1
Anda bahkan tidak memerlukan perilaku yang tidak terdefinisi untuk mengacaukan pemulung di c ++. Anda bisa, misalnya, membuat serial sebuah pointer ke file dan membacanya nanti. Sementara itu, proses Anda mungkin tidak mengandung penunjuk itu di mana pun di ruang alamatnya, sehingga pengumpul sampah dapat mengumpulkan objek itu, dan kemudian ketika Anda membatalkan tanda penunjuk pointer ... Aduh.
Bwmat
@Bwmat "Even"? Menulis pointer ke file seperti itu sepertinya agak ... dibuat-buat. Bagaimanapun, masalah serius yang sama menimpa pointer untuk menumpuk objek, mereka mungkin hilang ketika Anda membaca kembali pointer dari file di tempat lain dalam kode! Deserializing nilai pointer tidak valid adalah perilaku yang tidak terdefinisi, jangan lakukan itu.
hyde
Jika tentu saja, Anda harus berhati-hati jika melakukan hal seperti itu. Ini dimaksudkan sebagai contoh bahwa, secara umum, seorang pengumpul sampah tidak dapat bekerja 'dengan benar' dalam semua kasus di c ++ (tanpa mengubah bahasa)
Bwmat
1
@ gnasher729: Ehm, bukan? Point-end-point baik-baik saja?
Deduplicator