ARC

Penghitungan referensi deterministik dengan semantik move sebagai dasar manajemen memori Nim.

Daftar isi

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.

nim hitungan_referensi.nim
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"
bash
# kompilasi program hitungan referensi dengan ARC
nim c --mm:arc --hints:off hitungan_referensi.nim
# jalankan program hasil kompilasi
./hitungan_referensi
text Keluaran
b masih terpakai: id 7
catatan 7 dimusnahkan
program berakhir

Penugasan 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.

nim urutan_musnah.nim
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.

bash
# kompilasi program urutan pemusnahan dengan ARC
nim c --mm:arc --hints:off urutan_musnah.nim
# jalankan program hasil kompilasi
./urutan_musnah
text Keluaran
blok dalam berakhir
catatan 2 dimusnahkan
catatan 1 dimusnahkan
blok luar berakhir
catatan 3 dimusnahkan
program berakhir

Keluaran 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.

Semantik move memindahkan kepemilikan objek dari var a ke var bSebelum movevar avar bobjek id 7nilmove(a)Sesudah movevar avar bobjek id 7nil
Perpindahan kepemilikan objek pada operasi move

Program berikut menerapkan move pada variabel ref.

nim move_buffer.nim
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"
bash
# kompilasi program semantik move dengan ARC
nim c --mm:arc --hints:off move_buffer.nim
# jalankan program hasil kompilasi
./move_buffer
text Keluaran
a nil? true
b terpakai: id 7
catatan 7 dimusnahkan
program berakhir

Keluaran 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 =destroy yang 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 =destroy untuk 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 =destroy berjalan 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, dan move() memaksakannya secara eksplisit.

Referensi#