Memori manual

Mengelola memori manual dengan Pointer, alokasi stack dan heap, serta destructor.

Daftar isi

Mojo mengurus umur nilai secara otomatis melalui aturan kepemilikan, tetapi pustaka standarnya tetap menyediakan pengelolaan memori manual untuk pekerjaan tingkat rendah. Topik ini membahas Pointer untuk menunjuk nilai yang sudah ada, alokasi heap dengan alloc dan dealloc, destructor sebagai penjaga pembebasan, serta alternatif yang dikelola secara otomatis. Setelah topik ini dibaca, pilihan antara pengelolaan otomatis dan manual dapat dipertimbangkan dengan mengetahui mekanisme dan risikonya.

Pointer ke nilai yang sudah ada#

Pointer menyimpan alamat memori sebuah nilai. Tipe Pointer berada di prelude, sehingga dapat digunakan tanpa import. Cara paling langsung untuk membuatnya adalah Pointer(to=nilai), yaitu menunjuk variabel lokal yang hidup di stack. Operasi ptr[] membaca nilai pada alamat tersebut, sedangkan penugasan lewat ptr[] = ... menulis nilai langsung ke variabel yang ditunjuk.

Jalankan program berikut yang menunjuk sebuah variabel lokal dan menulis lewat pointernya.

mojo pointer-stack.mojo
def main():
    var nilai = 5
    var ptr = Pointer(to=nilai)
    print(ptr[])
    ptr[] = 41
    print(nilai)
bash
# jalankan program pointer ke variabel lokal
mojo run pointer-stack.mojo
text Keluaran
5
41

print pertama membaca lewat pointer, sedangkan print kedua membaca variabel aslinya. Penugasan lewat ptr[] membuat pembacaan lewat pointer dan pembacaan nilai sama-sama menghasilkan 41, karena keduanya mengarah ke memori yang sama. Tipe pointer mengikuti tipe nilai yang ditunjuk, sehingga pembacaan lewat ptr[] menghasilkan nilai dengan tipe aslinya tanpa konversi tambahan. Pointer hanya menyimpan alamat tanpa memegang kepemilikan, sehingga umur pointer yang berakhir tidak membebaskan nilai yang ditunjuk. Sebagian operasi pada modul memori berawalan unsafe, seperti unsafe_write dan unsafe_offset, untuk menandai bahwa compiler umumnya tidak menyertakan pemeriksaan batas maupun masa hidup pada operasi tersebut. Awalan itu bukan larangan, melainkan penanda tanggung jawab, yaitu ketepatan penggunaannya sepenuhnya menjadi urusan penulis kode.

Alokasi heap dengan alloc dan dealloc#

Alokasi heap mengambil blok memori langsung dari pengalokasi, tanpa wadah yang mengelola umurnya. Fungsi alloc menerima Layout yang menentukan tipe dan jumlah elemen, lalu mengembalikan Allocation. Metode unsafe_ptr() pada Allocation memberikan pointer awalnya, unsafe_offset(i) menggeser pointer sebanyak i elemen, dan pembacaan elemen memakai kurung siku pada pointer yang sudah digeser.

Jalankan program berikut yang menulis dan membaca empat sel Int di heap.

mojo alokasi-heap.mojo
from std.memory.alloc import alloc, dealloc, Layout

def main():
    var alokasi = alloc(Layout[Int](count=4))
    var ptr = alokasi.unsafe_ptr()
    for i in range(4):
        ptr.unsafe_offset(i).unsafe_write(i * 10)
    for i in range(4):
        print(ptr.unsafe_offset(i)[])
    dealloc(alokasi^)
bash
# jalankan program alokasi heap beserta pembebasannya
mojo run alokasi-heap.mojo
text Keluaran
0
10
20
30

Program menulis 0, 10, 20, dan 30 pada empat sel Int, lalu membacanya kembali lewat pointer yang digeser. Pemanggilan dealloc(alokasi^) membebaskan blok tersebut dan mengonsumsi Allocation-nya, sehingga alokasi yang sama tidak dapat dibebaskan dua kali. Operator ^ kembali berperan di sini, yaitu menyerahkan kepemilikan Allocation kepada dealloc. Pola ini menuntut kedisiplinan, karena compiler tidak mengingatkan bila pemanggilan dealloc dilupakan.

Destructor sebagai penjaga pembebasan#

Metode khusus __deinit__ adalah destructor, yaitu kode yang dijalankan ketika umur sebuah nilai berakhir. Parameter deinit menandai bahwa metode menerima nilai dalam keadaan terkonsumsi, sehingga field-nya boleh dipindahkan atau dibebaskan. Pola yang lazim adalah struct yang memegang Allocation dan memanggil dealloc di dalam destructor, sehingga pemakai struct tidak dapat lupa membebaskan memori.

Jalankan program berikut yang mengemas alokasi heap ke dalam struct yang memiliki destructor.

mojo buffer.mojo
from std.memory.alloc import alloc, dealloc, Layout, Allocation

struct Buffer:
    var alokasi: Allocation[Int]

    def __init__(out self, ukuran: Int):
        self.alokasi = alloc(Layout[Int](count=ukuran))

    def tulis(mut self, i: Int, nilai: Int):
        self.alokasi.unsafe_ptr().unsafe_offset(i).unsafe_write(nilai)

    def baca(self, i: Int) -> Int:
        return self.alokasi.unsafe_ptr().unsafe_offset(i)[]

    def __deinit__(deinit self):
        dealloc(self.alokasi^)
        print("memori buffer dibebaskan")

def main():
    var b = Buffer(4)
    b.tulis(0, 10)
    b.tulis(3, 30)
    print(b.baca(0), b.baca(3))
bash
# jalankan program buffer yang membebaskan memorinya di destructor
mojo run buffer.mojo
text Keluaran
memori buffer dibebaskan
10 30

Buffer mengalokasikan empat sel Int pada initializer, menulis dan membacanya lewat metode, lalu membebaskan alokasinya di __deinit__. Urutan keluarannya memperlihatkan perilaku yang penting, yaitu pesan dari destructor muncul sebelum angka 10 30. Umur b berakhir tepat setelah pemakaian terakhirnya, yaitu dua panggilan baca pada argumen print, sehingga destructor berjalan sebelum print sempat menulis angkanya. Waktu penghancuran yang mengikuti pemakaian terakhir, bukan akhir lingkup (scope), adalah perilaku bawaan Mojo, dan manfaatnya adalah pembebasan sumber daya yang dapat berlangsung lebih awal.

Alternatif yang dikelola otomatis#

Untuk memori yang umurnya mengikuti pemakaian, pustaka standar menyediakan ManagedAllocation, yaitu pembungkus Allocation yang menangani pembebasannya sendiri saat umurnya berakhir. Pemanggilan dealloc tidak ditulis, sehingga kebocoran akibat kelalaian hilang. Kode yang memakai ManagedAllocation tetap menggunakan unsafe_ptr() dan pembacaan lewat kurung siku, sehingga migrasi dari alokasi manual hanya menyentuh bagian pembuatan dan pembebasannya.

Jalankan program berikut yang menggantikan dealloc dengan pembungkus yang dikelola otomatis.

mojo managed.mojo
from std.memory.alloc import ManagedAllocation, alloc, Layout

def main():
    var m = ManagedAllocation(alloc(Layout[Int](count=4)))
    var ptr = m.unsafe_ptr()
    ptr.unsafe_offset(0).unsafe_write(6)
    print(ptr.unsafe_offset(0)[])
bash
# jalankan program alokasi dengan pengelolaan otomatis
mojo run managed.mojo
text Keluaran
6

ManagedAllocation menerima Allocation hasil alloc dalam keadaan terkonsumsi, dan hasil alloc langsung ditulis di dalam kurung konstruktor sehingga tidak ada variabel Allocation yang harus dibebaskan manual. Umur ManagedAllocation ditentukan aturan kepemilikan yang sama dengan nilai lain, sehingga pembebasan terjadi tepat setelah pemakaian terakhirnya. Perbedaannya dengan alokasi manual hanya ada pada siapa yang memanggil dealloc, sedangkan pola pembacaan dan penulisan lewat pointer tidak berubah sama sekali. Untuk program pada umumnya, tipe yang berpegang pada aturan kepemilikan tetap menjadi pilihan pertama, sedangkan alokasi manual menyisakan pekerjaan untuk pustaka tingkat rendah yang membutuhkan kendali penuh atas letak dan umur memorinya.

Kesalahan umum#

  • Memakai pointer setelah alokasinya dibebaskan. Compiler tidak menolak pemakaian ini karena operasi pointer tidak disertai pemeriksaan masa hidup, sehingga perilakunya tidak terdefinisi (undefined behavior), mulai dari nilai yang salah sampai kegagalan program. Perbaikannya adalah tidak menyimpan pointer melebihi umur alokasinya, atau memindahkan pengelolaan memori ke struct yang membebaskan memorinya di __deinit__ atau ke ManagedAllocation.
  • Menulis dealloc dua kali untuk alokasi yang sama. Pemanggilan kedua bekerja pada Allocation yang sudah terkonsumsi dan tidak lagi sah. Perbaikannya adalah memasangkan satu dealloc dengan satu alloc dalam satu alur, atau menyerahkan pembebasannya kepada destructor atau ManagedAllocation.

Ringkasan#

  • Pointer menyimpan alamat memori, dibuat dengan Pointer(to=...) untuk nilai yang sudah ada, dan dibaca atau ditulis lewat ptr[].
  • Alokasi heap memakai alloc dengan Layout dan wajib dibebaskan lewat dealloc, sedangkan ManagedAllocation membebaskan memorinya sendiri.
  • Destructor __deinit__ menjalankan pembebasan sumber daya saat umur nilai berakhir, dan waktunya mengikuti pemakaian terakhir nilai tersebut.

Referensi#