ARC adalah singkatan dari Automatic Reference Counting, model manajemen memori yang menghitung setiap rujukan ke sebuah objek dan membebaskan memorinya di titik yang pasti. Topik ini membahas mekanisme penghitungan referensi (reference counting), peran destruktor (destructor) sebagai penanda pemusnahan, serta semantik move (move semantics) yang memindahkan kepemilikan (ownership) objek tanpa menyalinnya. Semua contoh dikompilasi dengan sakelar --mm:arc agar perilaku ARC dapat diamati terpisah dari pengumpul siklus milik ORC. Model ini menjadi dasar ORC, pengelola bawaan Nim 2.0 yang menambahkan pengumpulan siklus di atas mekanisme ARC.
Cara ARC menghitung referensi#
Setiap nilai bertipe ref pada ARC membawa penghitung yang mencatat jumlah pemegang aktifnya. Penghitung naik ketika sebuah rujukan baru dibuat dan turun ketika pemegangnya hilang, misalnya karena variabel ditimpa, diisi nil, atau keluar dari lingkup (scope) program. Saat penghitung mencapai nol, objek dimusnahkan segera pada baris itu juga. Berbeda dari GC konvensional yang memeriksa memori secara berkala, ARC tidak pernah menunda pemusnahan. Ketepatan ini bertentangan dengan kesan umum bahwa GC selalu identik dengan jeda tiba-tiba. Pada program interaktif, pemusnahan yang menempel pada baris kode membuat waktu respons lebih mudah diprediksi, sedangkan pada sistem waktu nyata (realtime) sifat itu bahkan menjadi syarat. Konsekuensinya, pesan pada destruktor dapat dipercaya sebagai dokumentasi perilaku, karena urutan pemusnahan yang tercetak tidak berubah antar eksekusi.
Program berikut memperlihatkan perilaku tersebut. Variabel a menampung sebuah objek, b menunjuk objek yang sama, lalu kedua variabel dikosongkan satu per satu.
type
CatatanSuhu = object
id: int
proc `=destroy`(c: var CatatanSuhu) =
echo "catatan ", c.id, " dimusnahkan"
type
RefCatatan = ref CatatanSuhu
var a = RefCatatan(id: 7)
var b = a
a = nil
echo "b masih terpakai: id ", b.id
b = nil
echo "program berakhir"# kompilasi program hitungan referensi dengan ARC
nim c --mm:arc --hints:off hitungan_referensi.nim
# jalankan program hasil kompilasi
./hitungan_referensib masih terpakai: id 7
catatan 7 dimusnahkan
program berakhirPenugasan b = a membuat b menunjuk objek yang sama dan menaikkan penghitungnya menjadi dua. Baris a = nil hanya menurunkan penghitung menjadi satu, sehingga pesan pemusnahan belum muncul. Pesan catatan 7 dimusnahkan baru tercetak tepat setelah b = nil, ketika penghitung mencapai nol. Titik pemusnahan seperti ini berulang secara identik pada setiap eksekusi, dan sifat itulah yang disebut deterministik.
Destruktor dan urutan pemusnahan#
Destruktor (destructor) adalah proc bernama =destroy, satu bentuk hook yang dijalankan tepat sebelum objek dimusnahkan. Pada hook inilah pelepasan sumber daya ditulis, misalnya menutup berkas atau membebaskan memori mentah. Destruktor juga berguna sebagai alat pengamatan karena pesan yang dicetaknya menunjukkan kapan pemusnahan terjadi. Untuk tipe ref object, hook didefinisikan pada tipe objeknya, sehingga pola umumnya mendeklarasikan tipe objek dan alias ref-nya secara terpisah.
type
CatatanSuhu = object
id: int
suhu: float
proc `=destroy`(c: var CatatanSuhu) =
echo "catatan ", c.id, " dimusnahkan"
type
RefCatatan = ref CatatanSuhu
block:
let pagi = RefCatatan(id: 1, suhu: 27.5)
let sore = RefCatatan(id: 2, suhu: 30.1)
echo "blok dalam berakhir"
block:
let malam = RefCatatan(id: 3, suhu: 25.9)
echo "blok luar berakhir"
echo "program berakhir"Program di atas membuat tiga catatan di dalam dua blok block yang masing-masing membentuk lingkup sendiri. Kompilasi dan jalankan programnya dengan ARC.
# kompilasi program urutan pemusnahan dengan ARC
nim c --mm:arc --hints:off urutan_musnah.nim
# jalankan program hasil kompilasi
./urutan_musnahblok dalam berakhir
catatan 2 dimusnahkan
catatan 1 dimusnahkan
blok luar berakhir
catatan 3 dimusnahkan
program berakhirKeluaran memperlihatkan dua aturan sekaligus. Objek pada blok dalam dimusnahkan lebih dulu, dan variabel lokal dalam satu lingkup dimusnahkan dalam urutan terbalik dari penciptaannya, sehingga catatan 2 mendahului catatan 1. Blok luar menyusul, lalu program berakhir. Semua titik pemusnahan berada di akhir lingkup masing-masing tanpa penundaan, dan urutan yang sama muncul pada setiap eksekusi.
Semantik move dan kepemilikan#
Semantik move memindahkan kepemilikan sebuah nilai dari satu variabel ke variabel lain tanpa menyalin isi dan tanpa mengubah penghitung. Dokumentasi destruktor Nim menyatakan bahwa salinan dapat digantikan move apabila sumbernya tidak digunakan lagi setelah penugasan, dan analisis alir kontrol statis kompilator melakukan penggantian itu secara otomatis. Apabila niatnya ingin ditegaskan, system.move tersedia untuk memaksakan move secara eksplisit. Setelah move, variabel sumber berada pada keadaan kosong, dan untuk tipe ref keadaan itu berarti nil.
Diagram berikut merangkum perpindahan kepemilikan dari a ke b.
Program berikut menerapkan move pada variabel ref.
type
CatatanSuhu = object
id: int
proc `=destroy`(c: var CatatanSuhu) =
echo "catatan ", c.id, " dimusnahkan"
type
RefCatatan = ref CatatanSuhu
var a = RefCatatan(id: 7)
var b = move(a)
echo "a nil? ", a.isNil
echo "b terpakai: id ", b.id
b = nil
echo "program berakhir"# kompilasi program semantik move dengan ARC
nim c --mm:arc --hints:off move_buffer.nim
# jalankan program hasil kompilasi
./move_buffera nil? true
b terpakai: id 7
catatan 7 dimusnahkan
program berakhirKeluaran menegaskan dua hal. Nilai a.isNil bernilai true karena kepemilikan objek sudah berpindah ke b, dan pesan pemusnahan muncul tepat setelah b = nil karena b adalah pemegang terakhir. Dibandingkan dengan penugasan biasa yang menaikkan penghitung, move tidak menaikkan apa pun sehingga lebih murah sekaligus menjadikan pemegangnya tunggal. Kepemilikan tunggal ini pula yang membuat move menjadi cara idiomatis menyerahkan objek antarbagian program, misalnya dari fungsi pembuatan ke pemanggilnya. Nilai kembalian sebuah proc yang membangun objek baru berpindah ke variabel penerima tanpa salinan, sehingga pola fungsi pembangun tidak membawa biaya penghitungan tambahan. Bila sebuah objek besar hanya dioperkan antarvariabel tanpa niat berbagi, menyisipkan move di titik serah terima menjaga jumlah penghitungan tetap minimal.
Kesalahan umum#
Tiga kesalahan berikut paling sering muncul ketika ARC baru dipraktikkan.
- Mengharapkan pemusnahan segera setelah satu variabel dikosongkan. Pemusnahan terjadi ketika penghitung mencapai nol, dan pemegang lain yang masih menunjuk objek yang sama menahannya. Kosongkan seluruh pemegang atau pindahkan kepemilikannya dengan move.
- Menulis hook
=destroyyang hanya mencetak pesan tanpa melepaskan bidangnya. Untuk objek dengan bidang string atau seq berukuran besar, kebocoran memori (memory leak) yang ditimbulkan terasa nyata. Perbaikannya adalah memanggil=destroyuntuk setiap bidang yang membutuhkan pemusnahan di dalam destruktor buatan sendiri. - Mengandalkan ARC untuk struktur data yang saling merujuk. Siklus referensi (reference cycle) tidak pernah terkumpul di ARC karena penghitungnya tidak pernah mencapai nol. Nim menyediakan ORC sebagai pengelola yang melengkapi ARC dengan pengumpulan siklus.
Ringkasan#
- ARC menghitung rujukan ke setiap objek dan memusnakannya tepat saat penghitung mencapai nol, sehingga titik pemusnahan dapat diprediksi.
- Destruktor
=destroyberjalan pada titik pemusnahan tersebut, dalam urutan terbalik dari penciptaan variabel di dalam satu lingkup. - Destruktor buatan sendiri menggantikan pemusnahan bidang bawaan, sehingga bidang string, seq, atau ref harus dilepaskan secara eksplisit.
- Semantik move memindahkan kepemilikan tanpa salinan, sumbernya menjadi
nil, danmove()memaksakannya secara eksplisit.
Referensi#
- Destructors and move semantics — Nim documentation
— Aturan hook
=destroy, analisis move pada kompilator, dan peransystem.move. - Nim’s Memory Management — Nim documentation — Ringkasan model memori Nim, termasuk ARC dan posisinya di antara pilihan lainnya.