Demonstrasi pengumpulan sampah menjadi lebih cepat daripada manajemen memori manual

23

Saya sudah membaca di banyak tempat (heck, saya sudah bahkan ditulis jadi diri sendiri) bahwa pengumpulan sampah bisa (secara teoritis) lebih cepat dari manajemen memori manual.

Namun, menunjukkan jauh lebih sulit didapat daripada memberi tahu.
Saya belum pernah benar - benar melihat potongan kode yang menunjukkan efek ini dalam tindakan.

Adakah yang punya (atau tahu di mana saya bisa menemukan) kode yang menunjukkan keunggulan kinerja ini?

Mehrdad
sumber
5
masalah dengan GC adalah bahwa sebagian besar implementasi tidak deterministik sehingga 2 run dapat memiliki hasil yang sangat berbeda, belum lagi sulit untuk mengisolasi variabel yang tepat untuk dibandingkan
ratchet freak
@ scratchetfreak: Jika Anda tahu ada contoh yang hanya lebih cepat (katakanlah) 70% dari waktu, itu juga tidak masalah bagi saya. Pasti ada beberapa cara untuk membandingkan keduanya, dalam hal throughput setidaknya (latensi mungkin tidak akan berhasil).
Mehrdad
1
Yah, ini agak sulit karena Anda selalu bisa melakukan secara manual apa pun yang memberi GC keunggulan atas apa yang Anda lakukan secara manual. Mungkin lebih baik untuk membatasi ini ke "standar" alat manajemen memori manual (malloc () / free (), pointer yang dimiliki, pointer bersama dengan refcount, pointer lemah, tidak ada pengalokasi kustom)? Atau, jika Anda mengizinkan pengalokasi khusus (yang mungkin lebih realistis atau kurang realistis, tergantung pada jenis programmer yang Anda asumsikan), beri batasan pada upaya dimasukkan ke dalam pengalokasi tersebut. Jika tidak, strategi manual "salin apa yang dilakukan GC dalam kasus ini" selalu setidaknya secepat GC.
1
Dengan "menyalin apa yang dilakukan oleh GC" Saya tidak bermaksud "membangun GC Anda sendiri" (meskipun perhatikan bahwa ini secara teoritis dimungkinkan dalam C ++ 11 dan seterusnya, yang memperkenalkan dukungan opsional untuk GC). Aku berarti, seperti yang telah saya worded itu sebelumnya dalam komentar yang sama, "melakukan apa yang memberikan GC keunggulan atas apa yang Anda lakukan secara manual". Misalnya, jika pemadatan mirip Cheney banyak membantu aplikasi ini, Anda dapat secara manual mengimplementasikan skema alokasi + pemadatan yang serupa, dengan custom smart pointer untuk menangani perbaikan pointer. Juga, dengan teknik seperti tumpukan bayangan, Anda dapat melakukan akar menemukan di C atau C ++, dengan mengorbankan pekerjaan ekstra.
1
@Ike: Tidak apa-apa. Lihat mengapa saya menanyakan pertanyaan itu? Itu titik seluruh pertanyaan saya - orang datang dengan segala macam penjelasan yang harus masuk akal tapi semua orang tersandung ketika Anda meminta mereka untuk memberikan demonstrasi yang membuktikan apa yang mereka katakan adalah benar dalam praktek. Inti dari pertanyaan ini adalah untuk sekali dan untuk semua menunjukkan bahwa ini sebenarnya dapat terjadi dalam praktik.
Mehrdad

Jawaban:

26

Lihat http://blogs.msdn.com/b/ricom/archive/2005/05/10/416151.aspx dan ikuti semua tautan untuk melihat Rico Mariani vs Raymond Chen (keduanya pemrogram yang sangat kompeten di Microsoft) untuk menyelesaikannya . Raymond akan meningkatkan satu unmanaged, Rico akan menanggapi dengan mengoptimalkan hal yang sama di yang dikelola.

Dengan dasarnya nol upaya optimasi, versi berhasil dimulai berkali-kali lebih cepat dari manual. Akhirnya beat manual berhasil, tetapi hanya dengan mengoptimalkan ke tingkat yang kebanyakan programmer tidak ingin pergi ke. Dalam semua versi, penggunaan memori manual signifikan lebih baik daripada dikelola.

btilly
sumber
+1 untuk mengutip contoh aktual dengan kode :) walaupun penggunaan yang tepat dari konstruksi C ++ (seperti swap) tidak sulit, dan mungkin akan membawa Anda ke sana dengan mudah berdasarkan kinerja ...
Mehrdad
5
Anda mungkin bisa mengalahkan Raymond Chen pada kinerja. Saya yakin bahwa saya tidak bisa kecuali dia keluar karena sakit, saya bekerja lebih keras berkali-kali, dan saya beruntung. Saya tidak tahu mengapa dia tidak memilih solusi yang Anda pilih. Saya yakin dia punya alasan untuk itu
btilly
Saya menyalin kode Raymond di sini , dan untuk membandingkan, saya menulis versi saya sendiri di sini . File ZIP yang berisi file teks ada di sini . Di komputer saya, saya menjalankan dalam 14 ms dan Raymond berjalan dalam 21 ms. Kecuali saya melakukan sesuatu yang salah (yang mungkin), kode 215-line-nya 50% lebih lambat daripada implementasi 48-line saya, bahkan tanpa menggunakan file yang dipetakan di memori atau kolam memori khusus (yang ia gunakan). Milik saya setengah selama versi C #. Apakah saya salah, atau apakah Anda mengamati hal yang sama?
Mehrdad
1
@Mehrdad Mencabut salinan gcc lama di laptop ini, saya dapat melaporkan bahwa kode Anda atau kode-nya tidak dapat dikompilasi, apalagi melakukan apa pun dengannya. Fakta bahwa saya tidak menggunakan Windows kemungkinan menjelaskan hal itu. Tetapi mari kita asumsikan bahwa angka dan kode Anda benar. Apakah mereka melakukan hal yang sama pada kompiler dan komputer berumur satu dekade? (Lihat kapan blog itu ditulis.) Mungkin, mungkin tidak. Mari kita anggap itu benar, bahwa dia (menjadi seorang programmer C) tidak tahu bagaimana menggunakan C ++ dengan benar, dll. Apa yang tersisa dengan kita?
btilly
1
Kita dibiarkan dengan program C ++ yang masuk akal yang dapat diterjemahkan ke memori yang dikelola dan dipercepat. Tetapi di mana versi C ++ dapat dioptimalkan dan dipercepat lebih jauh. Yang kita semua sepakat adalah pola umum yang selalu terjadi ketika kode terkelola lebih cepat daripada tidak terkelola. Namun kami masih memiliki contoh nyata dari kode yang masuk akal dari seorang programmer yang baik yang lebih cepat dalam versi yang dikelola.
btilly
5

Aturan praktisnya adalah tidak ada makan siang gratis.

GC menghilangkan sakit kepala manajemen memori manual dan mengurangi kemungkinan membuat kesalahan. Ada beberapa situasi di mana beberapa strategi GC tertentu adalah solusi optimal untuk masalah tersebut, dalam hal ini Anda tidak akan membayar penalti untuk menggunakannya. Tetapi ada yang lain di mana solusi lain akan lebih cepat. Karena Anda selalu dapat mensimulasikan abstraksi yang lebih tinggi dari tingkat yang lebih rendah tetapi tidak sebaliknya Anda dapat secara efektif membuktikan bahwa tidak ada cara abstraksi yang lebih tinggi bisa lebih cepat daripada yang lebih rendah dalam kasus umum.

GC adalah kasus khusus manajemen memori manual

Mungkin banyak pekerjaan atau lebih banyak kesalahan cenderung untuk mendapatkan kinerja yang lebih baik secara manual tapi itu cerita yang berbeda.

Guy Sirton
sumber
1
Itu tidak masuk akal bagi saya. Untuk memberikan Anda beberapa contoh konkret: 1) pengalokasi dan hambatan penulisan dalam GC produksi adalah assembler yang ditulis tangan karena C terlalu tidak efisien sehingga bagaimana Anda mengalahkannya dari C, dan 2) eliminasi panggilan ekor adalah contoh dari optimasi dilakukan dalam bahasa tingkat tinggi (fungsional) yang tidak dilakukan oleh kompiler C dan, oleh karena itu, tidak dapat dilakukan dalam C. Stack walking adalah contoh lain dari sesuatu yang dilakukan di bawah tingkat C oleh bahasa tingkat tinggi.
Jon Harrop
2
1) Saya harus melihat kode khusus untuk berkomentar tetapi jika pengalokasi tulisan tangan / hambatan dalam assembler lebih cepat maka gunakan assembler tulisan tangan. Tidak yakin apa yang harus dilakukan dengan GC. 2) Coba lihat di sini: stackoverflow.com/a/9814654/441099 intinya bukanlah apakah beberapa bahasa non-GC dapat melakukan penghapusan rekursi ekor untuk Anda. Intinya adalah Anda dapat mengubah kode Anda menjadi secepat atau lebih cepat. Apakah kompiler beberapa bahasa tertentu dapat melakukan ini untuk Anda secara otomatis adalah masalah kenyamanan. Dalam abstraksi yang cukup rendah Anda selalu dapat melakukan ini sendiri jika Anda mau.
Guy Sirton
1
Contoh tail tail di C hanya berfungsi untuk kasus khusus fungsi yang memanggil dirinya sendiri. C tidak dapat menangani kasus umum fungsi yang saling memanggil. Menjatuhkan ke assembler dan mengasumsikan waktu tak terbatas untuk pengembangan adalah turing turing.
Jon Harrop
3

Sangat mudah untuk membangun situasi buatan di mana GC jauh lebih efisien daripada metode manual - hanya mengatur bahwa hanya ada satu "root" untuk pengumpul sampah, dan bahwa semuanya adalah sampah, sehingga langkah GC langsung selesai.

Jika Anda memikirkannya, itulah model yang digunakan saat sampah mengumpulkan memori yang dialokasikan untuk suatu proses. Prosesnya mati, semua ingatannya adalah sampah, kita sudah selesai. Bahkan secara praktis, proses yang dimulai, berjalan, dan mati tanpa meninggalkan jejak mungkin lebih efisien daripada yang dimulai dan berjalan selamanya.

Untuk program praktis, ditulis dalam bahasa dengan pengumpulan sampah, keuntungan dari pengumpulan sampah bukanlah kecepatan tetapi kebenaran, dan kesederhanaan.

ddyer
sumber
Jika mudah untuk membuat contoh buatan, maukah Anda menunjukkan yang sederhana?
Mehrdad
1
@Mrdrdad Dia memang menjelaskan yang sederhana. Tulis program di mana versi GC gagal melakukan menjalankan sampah sebelum keluar. Versi dikelola memori manual akan lebih lambat karena secara eksplisit melacak dan membebaskan barang.
btilly
3
@ btilly: "Tulis sebuah program di mana versi GC gagal melakukan menjalankan sampah sebelum keluar." ... gagal melakukan pengumpulan sampah di tempat pertama adalah kebocoran memori karena kurangnya GC yang berfungsi, bukan peningkatan kinerja karena adanya GC! Itu seperti memanggil abort()C ++ sebelum program keluar. Ini perbandingan yang tidak berarti; Anda bahkan tidak mengumpulkan sampah, Anda hanya membiarkan memori bocor. Anda tidak bisa mengatakan pengumpulan sampah lebih cepat (atau lebih lambat) jika Anda tidak memulai pengumpulan sampah ...
Mehrdad
Untuk membuat contoh ekstrem, Anda harus mendefinisikan sistem lengkap dengan manajemen heap dan heap Anda sendiri, yang akan menjadi proyek siswa yang hebat tetapi terlalu besar untuk masuk dalam margin ini. Anda akan melakukannya dengan cukup baik dengan menulis sebuah program yang mengalokasikan dan mendelokasi array ukuran acak, dengan cara yang dirancang untuk menjadi stres terhadap metode manajemen memori non-gc.
ddyer
3
@Madrdad Tidak begitu. Skenarionya adalah bahwa versi GC tidak pernah terjadi untuk mencapai ambang di mana ia akan menjalankan, bukan bahwa ia akan gagal untuk melakukan dengan benar pada kumpulan data yang berbeda. Itu sepele akan sangat baik untuk versi GC, meskipun bukan prediktor yang baik untuk kinerja akhirnya.
btilly
2

Harus dipertimbangkan bahwa GC bukan hanya strategi manajemen memori; itu juga membuat tuntutan pada seluruh desain bahasa dan lingkungan runtime, yang membebankan biaya (dan manfaat). Sebagai contoh, bahasa yang mendukung GC harus dikompilasi ke dalam bentuk di mana pointer tidak dapat disembunyikan dari pengumpul sampah, dan umumnya di mana mereka tidak dapat dibangun kecuali dengan primitif sistem yang dikelola dengan hati-hati. Pertimbangan lainnya adalah sulitnya mempertahankan jaminan waktu respons, karena GC menerapkan beberapa langkah yang harus dibiarkan berjalan hingga selesai.

Akibatnya, jika Anda memiliki bahasa yang mengumpulkan sampah, dan membandingkan kecepatan dengan memori yang dikelola secara manual di sistem yang sama, Anda masih harus membayar biaya overhead untuk mendukung pengumpulan sampah bahkan jika Anda tidak menggunakannya.

ddyer
sumber
2

Lebih cepat meragukan. Namun, itu bisa sangat cepat, tidak terlihat, atau lebih cepat jika perangkat kerasnya didukung. Ada desain seperti itu untuk mesin LISP sejak lama. Seseorang membangun GC ke dalam subsistem memori perangkat keras sehingga CPU utama tidak tahu ada di sana. Seperti banyak desain selanjutnya, GC berjalan bersamaan dengan prosesor utama dengan sedikit atau tanpa perlu jeda. Desain yang lebih modern adalah mesin-mesin Azul Systems Vega 3 yang menjalankan kode Java lebih cepat daripada JVM yang menggunakan prosesor yang dibuat khusus dan sebuah GC yang tidak ada jeda. Google mereka jika Anda ingin tahu seberapa cepat GC (atau Java) dapat.

Nick P
sumber
2

Saya telah melakukan sedikit pekerjaan dalam hal ini dan menjelaskan beberapa di sini . Saya membandingkan Boehm GC dalam C ++, mengalokasikan menggunakan malloctetapi tidak membebaskan, mengalokasikan dan membebaskan menggunakan freedan mark-region GC yang dibuat khusus ditulis dalam C ++ all vs OCaml's stock GC menjalankan pemecah n-queens berbasis daftar. GC OCaml lebih cepat dalam semua kasus. Program C ++ dan OCaml sengaja ditulis untuk melakukan alokasi yang sama dalam urutan yang sama.

Anda dapat, tentu saja, menulis ulang program untuk memecahkan masalah hanya menggunakan integer 64-bit dan tidak ada alokasi. Meskipun lebih cepat itu akan mengalahkan titik latihan (yang adalah untuk memprediksi kinerja algoritma GC baru saya sedang mengerjakan menggunakan prototipe yang dibangun di C ++).

Saya telah menghabiskan bertahun-tahun di industri porting kode C ++ nyata ke bahasa yang dikelola. Di hampir setiap kasus saya mengamati peningkatan kinerja yang substansial, banyak di antaranya mungkin disebabkan oleh manajemen memori manual yang lebih baik. Keterbatasan praktis bukanlah apa yang dapat dicapai dalam microbenchmark tetapi apa yang dapat dicapai sebelum batas waktu dan bahasa berbasis GC menawarkan peningkatan produktivitas yang sangat besar sehingga saya tidak pernah melihat ke belakang. Saya masih menggunakan C dan C ++ pada perangkat yang disematkan (mikrokontroler) tetapi bahkan itu sedang berubah sekarang.

Jon Harrop
sumber
+1 terima kasih. Di mana kita bisa melihat dan menjalankan kode benchmark?
Mehrdad
Kode tersebar tentang tempat itu. Saya memposting versi wilayah tanda di sini: groups.google.com/d/msg/…
Jon Harrop
1
Ada hasil untuk thread aman dan tidak aman di sana.
Jon Harrop
1
@Mehrdad: "Sudahkah Anda menghilangkan sumber kesalahan potensial tersebut?" Iya nih. OCaml memiliki model kompilasi yang sangat sederhana tanpa optimisasi seperti melarikan diri analisis. Representasi OCaml tentang penutupan sebenarnya jauh lebih lambat daripada solusi C ++ sehingga harus benar-benar menggunakan kebiasaan List.filterseperti halnya C ++. Tapi, ya, Anda tentu benar bahwa beberapa operasi RC dapat dielakkan. Namun, masalah terbesar yang saya lihat di alam liar adalah bahwa orang tidak punya waktu untuk melakukan optimasi seperti itu dengan tangan pada basis kode industri yang besar.
Jon Harrop
2
Ya, tentu saja. Tidak ada upaya tambahan untuk menulis tetapi menulis kode bukanlah hambatan dengan C ++. Mempertahankan kode adalah. Mempertahankan kode dengan kompleksitas insidental semacam ini adalah mimpi buruk. Kebanyakan basis kode industri adalah jutaan baris kode. Anda hanya tidak mau harus berurusan dengan itu. Saya telah melihat orang mengonversi semuanya menjadi shared_ptrhanya untuk memperbaiki bug konkurensi. Kode jauh lebih lambat tetapi, hei, sekarang berfungsi.
Jon Harrop
-1

Contoh seperti itu tentu saja memiliki skema alokasi memori manual yang buruk.

Asumsikan pengumpul sampah terbaik GC. Secara internal ia memiliki metode untuk mengalokasikan memori, menentukan memori apa yang dapat dibebaskan, dan metode untuk akhirnya membebaskannya. Bersama-sama ini membutuhkan waktu kurang dari semua GC; beberapa waktu dihabiskan dalam metode lain GC.

Sekarang pertimbangkan pengalokasi manual yang menggunakan mekanisme alokasi yang sama GC, dan yang free()panggilannya hanya menyisihkan memori untuk dibebaskan dengan metode yang sama seperti GC. Itu tidak memiliki fase pemindaian, juga tidak memiliki metode lain. Itu tentu membutuhkan waktu lebih sedikit.

MSalters
sumber
2
Seorang pengumpul sampah seringkali dapat membebaskan banyak objek, tanpa harus memasukkan memori ke dalam keadaan yang berguna setelah masing-masing. Pertimbangkan tugas untuk menghapus dari daftar-array semua item yang memenuhi kriteria tertentu. Menghapus satu item dari daftar N-item adalah O (N); menghapus item M dari daftar N, satu per satu adalah O (M * N). Menghapus semua item yang memenuhi kriteria dalam sekali melewati daftar, adalah O (1).
supercat
@supercat: freejuga bisa mengumpulkan batch. (Dan tentu saja menghapus semua item yang memenuhi kriteria masih O (N), jika hanya karena daftar traversal itu sendiri)
MSalters
Menghapus semua item yang memenuhi kriteria setidaknya O (N). Anda benar yang freedapat beroperasi dalam mode pengumpulan-kumpulan jika setiap item memori memiliki flag yang terkait dengannya, meskipun GC masih dapat unggul dalam beberapa situasi. Jika seseorang memiliki referensi M yang mengidentifikasi L item yang berbeda dari satu set N hal, waktu untuk menghapus setiap referensi yang tidak ada referensi dan menggabungkan sisanya adalah O (M) daripada O (N). Jika seseorang memiliki ruang ekstra M yang tersedia, konstanta penskalaan bisa sangat kecil. Selanjutnya, pemadatan dalam sistem GC non-pemindaian membutuhkan ...
supercat
@supercat: Yah, itu pasti bukan O (1) sebagai kalimat terakhir Anda di komentar pertama menyatakan.
MSalters
1
@ MSalters: "Dan apa yang akan mencegah skema deterministik dari memiliki kamar anak?". Tidak ada. Pengumpul sampah OCaml bersifat deterministik dan menggunakan pembibitan. Tapi ini bukan "manual" dan saya pikir Anda menyalahgunakan kata "deterministik".
Jon Harrop