Proyek Odin berjalan tanpa pengumpul sampah, sehingga keputusan memori menjadi tanggung jawab eksplisit penulisnya sejak baris pertama. Urutan kerja yang sehat menaruh rancangan data di depan logika perilaku, bukan sesudahnya. Topik ini menerapkan rutinitas rancang empat langkah pada proyek simulasi populasi kelinci, yaitu menetapkan entri data beserta ukurannya, menetapkan kepemilikan setiap data, memilih alokator menurut kebutuhan, dan mengestimasi kebutuhan memori sebelum logika ditulis. Seluruh keputusan itu terdokumentasi pada satu program utuh yang menjalankan lima generasi simulasi, mencetak statistik setiap generasi, dan diaudit oleh alokator pelacak pada akhir program.
Empat langkah rancang sebelum perilaku#
Perilaku program berubah cepat selama pengembangan, sedangkan tata letak data adalah kerangka yang dibaca dan dimutasi oleh seluruh perilaku tersebut. Menulis ulang logika berarti menyunting satu proc, sedangkan menyusun ulang data menular ke setiap proc yang menyentuhnya, sehingga keputusan data layak ditetapkan lebih dahulu. Pada bahasa tanpa pengumpul sampah, keputusan itu tidak dapat ditunda karena tiga hal yang biasanya disembunyikan oleh pengumpul sampah menjadi tanggung jawab eksplisit, yaitu tata letak field, kepemilikan data (ownership), dan pemilihan alokator.
Rutinitas rancang pada proyek ini terdiri atas empat langkah yang dijalankan sebelum satu baris logika simulasi ditulis.
- Daftarkan entri data dan field-nya lengkap dengan tipe dan ukurannya, lalu ukur dengan
size_of,align_of, danoffset_of. - Tetapkan kepemilikan setiap data, yaitu siapa yang membuat, siapa yang membebaskan, dan siapa yang membaca.
- Pilih alokator menurut umur pakai data, yaitu heap untuk data yang hidup lama, arena untuk data yang dibuang bersama-sama, dan temp untuk string sementara.
- Estimasikan kebutuhan memori dengan mengalikan jumlah entri terhadap
size_ofsetiap entri.
Empat langkah itu tidak berhenti sebagai dokumen. Pada proyek kelinci, langkah pertama dan keempat menjadi keluaran program, sedangkan langkah kedua dan ketiga menjadi komentar beserta disiplin defer di dalam kode. Rancangan yang dijalankan bersama programnya tidak mudah usang, karena setiap keputusan di kemudian hari dapat diukur ulang terhadap angka yang sama.
Entri data utama dan ukurannya#
Langkah pertama menetapkan entri data utama, yaitu satu struct Kelinci beserta field identitas dan keadaannya. Setiap field memakai tipe terkecil yang cukup untuk kebutuhannya. Field nomor bertipe u32 agar identitas tetap unik apabila batas populasi dinaikkan melampaui 255 entri, dan umur bertipe u8 karena kelinci mati pada umur lima. Field jenis dan status adalah enum bertipe dasar u8 karena masing-masing hanya punya dua varian, sedangkan generasi bertipe u8 karena simulasi hanya berjalan lima generasi. Urutan field disusun dari terbesar ke terkecil, dan tata letak yang dipilih diukur dengan tiga proc bawaan bahasa berikut.
// tidak-dijalankan
Kelinci :: struct {
nomor: u32,
umur: u8,
jenis: Jenis_Kelamin,
status: Status_Hidup,
generasi: u8,
}
fmt.printf("size_of(Kelinci) = %v byte, align_of = %v\n", size_of(Kelinci), align_of(Kelinci))
fmt.printf("offset nomor = %v, umur = %v, jenis = %v, status = %v, generasi = %v\n", offset_of(Kelinci, nomor), offset_of(Kelinci, umur), offset_of(Kelinci, jenis), offset_of(Kelinci, status), offset_of(Kelinci, generasi))Keluaran program pada bagian akhir menunjukkan size_of(Kelinci) bernilai 8 byte dengan alignment 4, sedangkan offset field berurutan rapat pada 0, 4, 5, 6, dan 7 tanpa padding. Angka itu dapat dihitung ulang, karena nomor menempati empat byte pertama dan empat field selebar satu byte mengisi byte keempat hingga kedelapan. Pembuktian dilakukan dengan mengubah urutan. Apabila nomor ditaruh di antara dua field u8 yang lebih kecil, alignment u32 memaksa dua byte padding dan ukuran struct membengkak menjadi 12 byte. Ketiga proc pengukuran bekerja saat kompilasi dan tidak menambah biaya apa pun, sehingga mengukur ulang tata letak aman dilakukan setiap kali satu field ditambahkan.
Kepemilikan data#
Langkah kedua menetapkan kepemilikan setiap data, yaitu siapa yang membuat, siapa yang membebaskan, dan siapa yang membaca. Penetapan itu ditulis sebagai komentar di dekat deklarasi data dan ditegakkan dengan disiplin defer. Program kelinci membagi kepemilikan kepada tiga pemilik berikut.
populasidimiliki procsimulasikan: dibuat melaluimake, dibebaskan melaluidefer deletepada baris bertetangga, dimutasi olehjalankan_generasi, dan dibaca olehhitung_statistiksertalaporkan_generasi.buf_arenadan structarenadimiliki procsimulasikan: buffer byte dibuat dan dibebaskan melaluidefer delete, sedangkanlaporkan_generasihanya meminjamnya melalui pointer.Tracking_Allocatordimiliki procmain: dipasang sebelumsimulasikandipanggil, dan map internalnya dibebaskan olehtracking_allocator_destroypada akhir program.
String laporan adalah kasus khusus karena pemiliknya adalah arena, bukan proc mana pun. Setiap string lahir dari fmt.aprintf di dalam laporkan_generasi, dicetak, lalu mati pada arena_free_all sebelum proc yang sama berakhir. Disiplin defer menjaga pola itu tetap benar ketika proc bertambah kompleks, yaitu defer delete ditulis tepat di bawah make-nya, sedangkan defer context = ctx_awal ditulis tepat di bawah penyimpanan salinan context. Karena defer dieksekusi terbalik dari urutan pendaftarannya, pemulihan context pada main berjalan sebelum tracking_allocator_destroy, sehingga map internal dibebaskan melalui alokator yang sama dengan pembuatnya.
Pemilihan alokator per kebutuhan data#
Langkah ketiga memetakan setiap data ke alokator yang cocok dengan umur pakainya, dan ukuran data sama sekali bukan kriterianya. Tabel berikut memuat pemetaan yang ditetapkan sebelum logika simulasi ditulis.
| Data | Alokator | Alasan |
|---|---|---|
populasi | Heap bawaan (context.allocator) | Hidup sepanjang simulasi dan tumbuh per generasi, sehingga membutuhkan pelepasan per alokasi |
| String laporan per generasi | Arena mem.Arena di atas satu buffer byte | Seluruh laporan satu generasi mati bersama, sehingga cukup satu panggilan arena_free_all |
| String sementara di luar rancangan ini | context.temp_allocator | Umur terpendek, cocok untuk hasil format yang tidak keluar dari satu baris |
Arena dipilih untuk laporan bukan karena ukurannya kecil, melainkan karena umurnya dapat diikat ke satu generasi. Setiap panggilan laporkan_generasi memasang mem.arena_allocator pada context, membangun string laporannya, mencetaknya, lalu mengosongkan arena dalam satu panggilan arena_free_all. Pemakaian arena juga terukur dari luar melalui field offset, sehingga keputusan alokator dapat diaudit seperti keputusan lainnya. Heap bawaan tetap dipilih untuk populasi karena array dinamis tersebut tumbuh dari 4 menjadi 124 entri dan harus tetap hidup hingga generasi kelima, sesuatu yang tidak dapat ditawarkan oleh arena yang dikosongkan pada setiap generasi. Pertumbuhan yang lahir dari append mendarat pada context.allocator yang sedang aktif, dan proc mutasi hanya dipanggil di luar lingkup arena.
Program simulasi populasi kelinci#
Program utuh berikut menggabungkan keempat langkah rancangan. Strukturnya terbaca sebagai lima bagian, yaitu deklarasi entri data di atas berkas, proc mutasi jalankan_generasi, proc pembaca hitung_statistik, proc pelapor laporkan_generasi, lalu pemilik data simulasikan dan auditor main. Simpan berkasnya sebagai kelinci.odin di dalam folder topik dan jalankan dari folder yang sama.
package main
import "core:fmt"
import "core:mem"
// LANGKAH RANCANG 1: entri data utama beserta field-nya.
// Enum diberi tipe dasar u8 karena hanya punya dua varian,
// dan field disusun dari terbesar ke terkecil agar padding nol.
Jenis_Kelamin :: enum u8 {
Jantan,
Betina,
}
Status_Hidup :: enum u8 {
Hidup,
Mati,
}
Kelinci :: struct {
nomor: u32,
umur: u8,
jenis: Jenis_Kelamin,
status: Status_Hidup,
generasi: u8,
}
// batas populasi yang dirancang menjadi dasar estimasi memori
BATAS_POPULASI :: 128
// mutasi data utama, sehingga pertumbuhan array mendarat pada context.allocator
// karena proc ini hanya dipanggil di luar lingkup arena
jalankan_generasi :: proc(populasi: ^[dynamic]Kelinci, generasi: int) -> (lahir, mati: int) {
// penuaan: kelinci hidup bertambah umur, umur lima berarti mati
for i in 0 ..< len(populasi) {
if populasi[i].status == .Hidup {
populasi[i].umur += 1
if populasi[i].umur >= 5 {
populasi[i].status = .Mati
mati += 1
}
}
}
// kelahiran: setiap betina hidup berumur minimal satu melahirkan sepasang anak
for i in 0 ..< len(populasi) {
induk := populasi[i]
if induk.status == .Hidup && induk.jenis == .Betina && induk.umur >= 1 {
nomor := u32(len(populasi) + 1)
append(populasi, Kelinci{nomor = nomor, umur = 0, jenis = .Jantan, status = .Hidup, generasi = u8(generasi)})
append(populasi, Kelinci{nomor = nomor + 1, umur = 0, jenis = .Betina, status = .Hidup, generasi = u8(generasi)})
lahir += 2
}
}
return
}
// statistik dibaca dari data utama, dan proc ini tidak mengalokasikan apa pun
hitung_statistik :: proc(populasi: []Kelinci) -> (hidup, jantan, betina: int) {
for k in populasi {
if k.status == .Hidup {
hidup += 1
if k.jenis == .Jantan {
jantan += 1
} else {
betina += 1
}
}
}
return
}
// laporan hidup di arena: dibangun, dicetak, lalu dibuang sekaligus
laporkan_generasi :: proc(arena: ^mem.Arena, generasi: int, populasi: []Kelinci, lahir, mati: int) {
ctx_awal := context
context.allocator = mem.arena_allocator(arena)
defer context = ctx_awal
hidup, jantan, betina := hitung_statistik(populasi)
laporan := fmt.aprintf("generasi %v: hidup %v dari %v entri (jantan %v, betina %v), lahir %v, mati %v", generasi, hidup, len(populasi), jantan, betina, lahir, mati)
fmt.println(laporan)
fmt.printf("arena generasi %v: %v byte terpakai\n", generasi, arena.offset)
// seluruh string laporan generasi ini dibuang bersama-sama
mem.arena_free_all(arena)
}
// pemilik data simulasi: populasi dan buffer arena lahir dan mati di proc ini
simulasikan :: proc() {
// data utama hidup lama, dialokasikan dari context.allocator (heap)
// dan kapasitasnya direservasi sejak awal sesuai estimasi
populasi := make([dynamic]Kelinci, 0, BATAS_POPULASI)
defer delete(populasi)
fmt.printf("rancang: kapasitas populasi %v entri, alokasi %v byte\n", cap(populasi), cap(populasi) * size_of(Kelinci))
// arena laporan hidup satu generasi, direset pada setiap akhir generasi
buf_arena := make([]byte, mem.Kilobyte)
defer delete(buf_arena)
arena: mem.Arena
mem.arena_init(&arena, buf_arena)
// populasi awal: dua jantan dan dua betina yang baru lahir
append(&populasi, Kelinci{nomor = 1, umur = 0, jenis = .Jantan, status = .Hidup, generasi = 0})
append(&populasi, Kelinci{nomor = 2, umur = 0, jenis = .Jantan, status = .Hidup, generasi = 0})
append(&populasi, Kelinci{nomor = 3, umur = 0, jenis = .Betina, status = .Hidup, generasi = 0})
append(&populasi, Kelinci{nomor = 4, umur = 0, jenis = .Betina, status = .Hidup, generasi = 0})
for generasi in 1 ..= 5 {
lahir, mati := jalankan_generasi(&populasi, generasi)
laporkan_generasi(&arena, generasi, populasi[:], lahir, mati)
}
}
main :: proc() {
// LANGKAH RANCANG 1 dan 4: ukur tata letak, lalu estimasikan memorinya
fmt.printf("rancang: size_of(Kelinci) = %v byte, align_of = %v\n", size_of(Kelinci), align_of(Kelinci))
fmt.printf("rancang: offset nomor = %v, umur = %v, jenis = %v, status = %v, generasi = %v\n", offset_of(Kelinci, nomor), offset_of(Kelinci, umur), offset_of(Kelinci, jenis), offset_of(Kelinci, status), offset_of(Kelinci, generasi))
fmt.printf("rancang: estimasi populasi penuh %v x %v = %v byte\n", BATAS_POPULASI, size_of(Kelinci), BATAS_POPULASI * size_of(Kelinci))
// audit: tracking allocator membungkus heap bawaan sepanjang simulasi
ta: mem.Tracking_Allocator
mem.tracking_allocator_init(&ta, context.allocator)
defer mem.tracking_allocator_destroy(&ta)
ctx_awal := context
context.allocator = mem.tracking_allocator(&ta)
defer context = ctx_awal
simulasikan()
// seluruh defer di simulasikan sudah berjalan, sehingga audit bersih
fmt.printf("audit: total alokasi %v byte dalam %v permintaan\n", ta.total_memory_allocated, ta.total_allocation_count)
fmt.printf("audit: total pelepasan %v byte dalam %v pelepasan\n", ta.total_memory_freed, ta.total_free_count)
fmt.printf("audit: memori hidup saat audit %v byte dalam %v entri\n", ta.current_memory_allocated, len(ta.allocation_map))
}# sumberkan lingkungan sandbox jalur odin bila belum tersedia
[ -f /home/kal/my-project/tools/env.sh ] && source /home/kal/my-project/tools/env.sh
# jalankan program dari dalam folder topik
odin run kelinci.odin -filerancang: size_of(Kelinci) = 8 byte, align_of = 4
rancang: offset nomor = 0, umur = 4, jenis = 5, status = 6, generasi = 7
rancang: estimasi populasi penuh 128 x 8 = 1024 byte
rancang: kapasitas populasi 128 entri, alokasi 1024 byte
generasi 1: hidup 8 dari 8 entri (jantan 4, betina 4), lahir 4, mati 0
arena generasi 1: 244 byte terpakai
generasi 2: hidup 16 dari 16 entri (jantan 8, betina 8), lahir 8, mati 0
arena generasi 2: 224 byte terpakai
generasi 3: hidup 32 dari 32 entri (jantan 16, betina 16), lahir 16, mati 0
arena generasi 3: 224 byte terpakai
generasi 4: hidup 64 dari 64 entri (jantan 32, betina 32), lahir 32, mati 0
arena generasi 4: 224 byte terpakai
generasi 5: hidup 120 dari 124 entri (jantan 60, betina 60), lahir 60, mati 4
arena generasi 5: 224 byte terpakai
audit: total alokasi 2048 byte dalam 2 permintaan
audit: total pelepasan 2048 byte dalam 2 pelepasan
audit: memori hidup saat audit 0 byte dalam 0 entriBaris rancang pada keluaran adalah hasil langkah pertama dan keempat yang dicetak sebelum simulasi berjalan. Entri Kelinci berukuran 8 byte, estimasi populasi penuh menghasilkan 1024 byte, dan reservasi kapasitas pada make benar-benar mengalokasikan 1024 byte itu sekaligus, sehingga append tidak pernah menggeser memori selama simulasi. Lima baris generasi menunjukkan populasi tumbuh dari 4 menjadi 124 entri, dengan empat kelinci pendiri mati pada generasi kelima karena mencapai umur lima, dan populasi akhir itu masih berada di dalam batas 128 yang dirancang. Pemakaian arena per generasi berkisar 224 hingga 244 byte dari buffer berukuran 1024 byte.
Tiga baris audit membuktikan disiplin kepemilikan dengan angka. Total alokasi 2048 byte dalam dua permintaan, yaitu 1024 byte untuk populasi dan 1024 byte untuk buf_arena, dibebaskan seluruhnya dalam dua pelepasan sehingga memori hidup saat audit bernilai nol. Pelacak membungkus heap bawaan, sedangkan alokasi di dalam arena tidak masuk laporan karena arena memakai buffer yang sudah tercatat, dan pemisahan itu memang dirancang demikian.
Kesalahan umum#
Kesalahan pertama adalah menyatukan mutasi data utama dengan lingkup arena. Apabila pemanggilan jalankan_generasi berada di dalam lingkup yang alokatornya arena, pertumbuhan append mengambil memori dari arena, dan arena_free_all pada akhir generasi mengosongkan memori penopang array. Data populasi yang seharusnya bertahan kemudian tertimpa alokasi generasi berikutnya, sehingga statistik menjadi salah tanpa pesan error apa pun. Perbaikannya adalah memisahkan lingkup secara tegas, yaitu proc mutasi hanya dipanggil di luar lingkup arena, dan pemulihan context melalui defer menjaga agar tidak ada alokasi yang mendarat pada alokator yang salah.
Kesalahan kedua adalah membiarkan context masih memegang alokator arena ketika lingkup laporan berakhir. Semua alokasi setelah lingkup itu, termasuk pertumbuhan append dan pemanggilan delete, ditujukan ke alokator arena. Mode Free arena tidak dilaksanakan, sehingga delete menjadi tanpa efek dan laporan pelacak menumpuk entri yang tidak pernah dilepaskan. Perbaikannya sudah tampak pada program, yaitu menyimpan salinan context ke ctx_awal, memasang alokator arena, lalu menulis defer context = ctx_awal pada baris yang bertetangga.
Ringkasan#
- Rancangan data dikerjakan sebelum perilaku, mencakup entri beserta ukurannya melalui
size_ofdanoffset_of, kepemilikan, alokator, dan estimasi memori. - Kepemilikan dinyatakan lewat komentar dan disiplin
defer, yaitumakeberpasangan dengandefer deletepada baris bertetangga dan context dipulihkan lewatdefer. - Alokator dipilih menurut umur pakai data, yaitu heap untuk data utama yang hidup lama, arena untuk laporan per generasi yang dibuang bersama, dan temp untuk string sementara.
- Estimasi 128 entri dikali 8 byte memandu reservasi kapasitas populasi, sedangkan alokator pelacak membuktikan alokasi dan pelepasan seimbang pada 2048 byte.
Referensi#
- Overview — Odin Documentation — Panduan resmi bagian struct, sistem context, dan allocators yang menjadi dasar rutinitas rancangan.
- Paket core:mem — Referensi pustaka Odin
— Dokumentasi
Arena,arena_init,arena_allocator, danTracking_Allocatoryang dipakai program kelinci.