
Hampir semua program non-sepele mengalokasikan dan menggunakan memori dinamis. Melakukannya dengan benar menjadi semakin penting karena program menjadi lebih kompleks dan kesalahan bahkan lebih mahal.
Masalah umum adalah:
- kebocoran memori (tidak membebaskan memori yang terpakai)
- rilis ganda (rilis memori lebih dari sekali)
- gunakan setelah rilis (penggunaan pointer ke memori yang sebelumnya dibebaskan)
Tantangannya adalah untuk melacak pointer yang bertanggung jawab untuk membebaskan memori (yaitu, mereka yang memiliki memori), dan untuk membedakan pointer yang hanya menunjuk ke sepotong memori, mengontrol di mana mereka berada, dan mana dari mereka yang aktif (dalam ruang lingkup).
Solusi umum adalah sebagai berikut:
- Garbage Collection (GC) - GC memiliki blok memori dan secara berkala memindai mereka untuk petunjuk ke blok ini. Jika tidak ada petunjuk yang ditemukan, memori dibebaskan. Skema ini dapat diandalkan dan digunakan dalam bahasa seperti Go dan Java. Tetapi GC cenderung menggunakan lebih banyak memori daripada yang diperlukan, telah jeda dan memperlambat kode karena pengemasan ulang (gerbang tulis orig.inserted).
- Reference Counting (RC) - Objek RC memiliki memori dan menyimpan counter pointer ke dirinya sendiri. Ketika penghitung ini berkurang ke nol, memori dibebaskan. Ini juga merupakan mekanisme yang dapat diandalkan dan diterima dalam bahasa seperti C ++ dan ObjectiveC. RC adalah memori efisien, selain itu hanya membutuhkan ruang di bawah meja. Aspek negatif RC adalah overhead untuk mempertahankan counter, menanamkan handler pengecualian untuk memastikan pengurangannya, dan memblokir yang diperlukan untuk objek yang dibagikan di antara aliran program. Untuk meningkatkan produktivitas, pemrogram terkadang menipu diri sendiri dengan merujuk sementara ke objek RC yang melewati penghitung, sehingga menimbulkan risiko salah melakukannya.
- Kontrol manual - Manajemen memori manual adalah Sysalny malloc dan gratis. Ini cepat dan efisien dalam hal penggunaan memori, tetapi bahasa tidak membantu melakukan semuanya dengan benar, sepenuhnya bergantung pada pengalaman dan semangat programmer. Saya telah menggunakan malloc dan gratis selama 35 tahun, dan dengan bantuan pengalaman pahit dan tak berujung saya jarang membuat kesalahan. Tapi ini bukan cara yang bisa diandalkan teknologi pemrograman, dan perhatikan bahwa saya mengatakan "jarang" dan bukan "tidak pernah."
Solusi 2 dan 3 sampai derajat tertentu mengandalkan kepercayaan pada programmer untuk melakukan semuanya dengan benar. Sistem yang berdasarkan pada iman tidak memiliki skala yang baik, dan kesalahan manajemen memori terbukti sangat sulit untuk diperiksa ulang (sangat buruk sehingga beberapa standar pengkodean tidak memungkinkan penggunaan memori dinamis).
Tetapi ada juga cara keempat - Kepemilikan dan Peminjaman, OB. Ini efisien memori, secepat operasi manual, dan dapat diperiksa ulang secara otomatis. Metode baru-baru ini telah dipopulerkan oleh bahasa pemrograman Rust. Ini juga memiliki kelemahan, khususnya kebutuhan untuk memikirkan kembali perencanaan algoritma dan struktur data.
Anda dapat menangani aspek-aspek negatif, dan sisa artikel ini adalah deskripsi skematis tentang bagaimana sistem OB bekerja dan bagaimana kami mengusulkan untuk menulisnya ke dalam bahasa D. Saya awalnya menganggap ini tidak mungkin, tetapi setelah menghabiskan beberapa waktu untuk berpikir, saya menemukan cara. Ini mirip dengan apa yang kami lakukan dengan pemrograman fungsional - dengan imutabilitas transitif dan fungsi "murni".
Kepemilikan
Keputusan siapa yang memiliki objek dalam memori sangat sederhana - hanya ada satu pointer ke objek dan itu adalah pemiliknya. Dia juga bertanggung jawab atas pelepasan memori, setelah itu menjadi tidak valid. Karena fakta bahwa pointer ke objek di memori adalah pemilik, tidak ada petunjuk lain di dalam struktur data ini, dan oleh karena itu struktur data membentuk pohon.
Konsekuensi kedua adalah bahwa pointer menggunakan semantik gerakan daripada menyalin:
T* f(); void g(T*); T* p = f(); T* q = p;
Menghapus pointer dari dalam struktur data dilarang:
struct S { T* p; } S* f(); S* s = f(); T* q = sp;
Mengapa tidak menandai sp sebagai tidak valid? Masalahnya adalah bahwa ini akan memerlukan pengaturan label di runtime, tetapi harus diselesaikan pada tahap kompilasi, karena itu hanya dianggap sebagai kesalahan kompilasi.
Keluarnya pointer sendiri di luar cakupan juga merupakan kesalahan:
void h() { T* p = f(); }
Anda harus memindahkan nilai pointer secara berbeda:
void g(T*); void h() { T* p = f(); g(p);
Ini menyelesaikan masalah kebocoran memori dan penggunaan setelah membebaskan (Petunjuk: untuk kejelasan, ganti f () dengan malloc (), dan g () dengan gratis ().)
Semua ini dapat diverifikasi pada tahap kompilasi menggunakan teknik
Analisis Aliran Data (DFA) , sangat mirip dengan itu digunakan untuk
menghilangkan subekspresi umum . DFA dapat menghilangkan kusut tikus dari transisi program yang mungkin timbul.
Meminjam
Sistem penguasaan yang dijelaskan di atas dapat diandalkan, tetapi terlalu ketat.
Pertimbangkan:
struct S { void car(); void bar(); } struct S* f(); S* s = f(); s.car();
Agar ini berfungsi, s.car () harus memiliki cara untuk mendapatkan pointer kembali saat keluar.
Beginilah cara kerja pinjaman. s.car () mengambil salinan s selama durasi s.car (). s tidak valid saat runtime, dan menjadi valid lagi ketika s.car () keluar.
Dalam D, fungsi anggota
struct mendapatkan pointer
ini dengan referensi, sehingga kami dapat mengadaptasi pinjaman dengan ekstensi kecil: mendapatkan argumen dengan referensi mengambilnya.
D juga mendukung ruang untuk pointer, jadi meminjam itu wajar:
void g(scope T*); T* f(); T* p = f(); g(p);
(Ketika fungsi menerima argumen dengan referensi atau pointer dengan ruang lingkup digunakan, mereka dilarang untuk melampaui batas fungsi atau ruang lingkup. Ini sesuai dengan semantik meminjam.)
Meminjam dengan cara ini menjamin keunikan pointer ke objek di memori pada saat tertentu.
Meminjam dapat diperluas lebih lanjut dengan pemahaman bahwa sistem kepemilikan juga dapat diandalkan, bahkan jika suatu objek juga ditunjukkan oleh beberapa pointer konstan (tetapi hanya satu yang bisa berubah). Pointer konstan tidak dapat mengubah memori atau membebaskannya. Ini berarti bahwa beberapa pointer konstan dapat dipinjam dari pemilik yang bisa berubah, tetapi ia tidak memiliki hak untuk digunakan ketika pointer konstan ini masih hidup.
Sebagai contoh:
T* f(); void g(T*); T* p = f();
Prinsip
Hal tersebut di atas dapat direduksi menjadi pemahaman berikut bahwa objek dalam ingatan berperilaku seolah-olah berada di salah satu dari dua keadaan:
- ada persis satu pointer yang bisa berubah untuk itu
- satu atau lebih pointer konstan tambahan
Pembaca yang penuh perhatian akan melihat sesuatu yang aneh dalam apa yang saya tulis: "seolah-olah". Apa yang ingin saya beri petunjuk? Apa yang sedang terjadi Ya ada satu. Bahasa pemrograman komputer penuh dengan "seolah-olah" di bawah tenda, sesuatu seperti uang di rekening bank Anda sebenarnya tidak ada di sana (saya minta maaf jika ini merupakan kejutan besar bagi seseorang), dan ini tidak berbeda dari itu. Baca terus!
Tapi pertama-tama, sedikit lebih dalam ke topik.
Mengintegrasikan Kepemilikan / Teknik Pinjam di D
Bukankah teknik-teknik ini tidak sesuai dengan cara orang biasanya menulis dalam D, dan tidak akan hampir semua program D yang ada rusak? Dan itu tidak mudah untuk diperbaiki, tetapi sangat banyak sehingga Anda harus mendesain ulang semua algoritma dari awal?
Ya memang. Kecuali D memiliki (hampir) senjata rahasia: atribut fungsi. Ternyata semantik kepemilikan / pinjaman (OB) dapat diimplementasikan untuk setiap fungsi secara terpisah setelah analisis semantik yang biasa. Pembaca yang penuh perhatian dapat melihat bahwa tidak ada sintaks baru telah ditambahkan, hanya pembatasan telah diberlakukan pada kode yang ada. D sudah memiliki riwayat menggunakan atribut fungsi untuk mengubah semantiknya, misalnya atribut
murni untuk membuat fungsi "murni". Untuk mengaktifkan semantik OB, atribut @
live ditambahkan.
Ini berarti bahwa OB dapat ditambahkan ke kode pada D secara bertahap, sesuai kebutuhan dan sumber daya gratis. Hal ini memungkinkan untuk menambahkan OB, dan ini sangat penting, terus-menerus mendukung proyek dalam keadaan berfungsi, diuji, dan siap-rilis sepenuhnya. Ini juga memungkinkan Anda untuk mengotomatiskan proses pemantauan berapa persentase proyek yang telah ditransfer ke OB. Teknik ini ditambahkan ke daftar jaminan bahasa D lainnya mengenai keandalan bekerja dengan memori (seperti mengendalikan non-distribusi pointer ke variabel sementara pada stack).
Seolah-olah
Beberapa hal yang perlu tidak dapat diwujudkan dengan kepatuhan ketat terhadap OB, seperti referensi penghitungan objek. Bagaimanapun, objek RC dirancang untuk memiliki banyak petunjuk. Karena objek RC aman ketika bekerja dengan memori (jika diterapkan dengan benar), objek RC dapat digunakan bersama dengan OB tanpa mempengaruhi keandalan secara negatif. Mereka tidak dapat dibuat menggunakan teknik OB. Solusinya adalah bahwa ada atribut fungsi lainnya di D, seperti @
system . @
system adalah fitur di mana banyak pemeriksaan keandalan dinonaktifkan. Secara alami, OB juga akan dinonaktifkan dalam kode dengan
sistem @. Di sinilah implementasi teknologi RC bersembunyi dari kontrol OB.
Tetapi dalam kode dengan OB, RC objek tampak seolah-olah mengikuti semua aturan, jadi tidak masalah!
Dibutuhkan sejumlah tipe pustaka yang serupa agar berhasil dengan OB.
Kesimpulan
Artikel ini adalah ikhtisar dasar teknologi OB. Saya sedang mengerjakan spesifikasi yang jauh lebih terperinci. Mungkin saja saya melewatkan sesuatu dan suatu lubang di bawah permukaan air, tetapi sejauh ini semuanya terlihat baik. Ini adalah perkembangan yang sangat menarik untuk D dan saya berharap untuk mengimplementasikannya.
Untuk diskusi dan komentar lebih lanjut dari Walter, lihat topik di
/ r / pemrograman subreddit dan di
Hacker News .