Antarmuka Allocator

Mengalokasikan memori secara eksplisit melalui antarmuka Allocator.

Daftar isi

Memori dinamis pada Zig tidak pernah diambil secara diam-diam, melainkan diminta melalui antarmuka std.mem.Allocator. Topik ini membahas isi antarmuka tersebut, alokasi pertama dengan std.heap.page_allocator, konvensi parameter allocator pada fungsi, serta kepasangan antara alokasi dan pembebasan. Setelah topik ini dibaca, buffer dapat dialokasikan, diisi, dibaca, dan dibebaskan dengan pola yang menjadi dasar seluruh topik berikutnya.

Konsep#

Allocator sebagai antarmuka memori#

Tipe std.mem.Allocator adalah antarmuka, yaitu sekumpulan metode yang sama bagi setiap penyedia memori tanpa membedakan cara kerja di baliknya. Metode yang paling sering dipakai adalah alloc untuk memperoleh blok baru, free untuk mengembalikannya, dupe untuk menyalin data, dupeSentinel untuk salinan yang berakhir dengan nilai penanda, dan resize untuk mengubah panjang. Pemanggilan alloc(u8, 256) mengembalikan slice []u8 berpanjang 256 yang siap ditulis.

Alokasi memori dapat gagal karena memori terbatas, dan kegagalan itu dimodelkan sebagai error. Tipe kembalian alloc adalah error union ![]T yang membawa error.OutOfMemory bila permintaan tidak dapat dipenuhi, sehingga pemanggil wajib menanganinya, umumnya dengan try. Model ini menjaga alokasi tetap eksplisit pada dua sisi, yaitu penyedia memorinya pada parameter dan kemungkinan gagalnya pada tipe kembalian.

Zig tidak memiliki allocator bawaan yang bekerja tanpa sepengetahuan penulisnya. Dokumentasi resmi menyatakan konvensi ini dengan tegas, yaitu fungsi yang membutuhkan memori menerima parameter Allocator, dan pustaka standar mengikutinya secara konsisten. Konsekuensinya, setiap titik alokasi terlihat pada kode sumber, karena allocator harus diteruskan secara tertulis ke setiap fungsi yang membutuhkannya.

Contoh Lengkap#

Alokasi pertama dengan page_allocator#

Pustaka standar menyediakan beberapa allocator dengan strategi berbeda, dan std.heap.page_allocator adalah yang paling sederhana. Allocator ini meminta memori langsung dari sistem operasi dalam satuan halaman setiap kali diminta, tanpa lapisan daur ulang apa pun. Untuk contoh pembelajaran dan alokasi yang jarang terjadi, page_allocator sudah memadai, sedangkan program serius biasanya memilih strategi lain yang lebih hemat panggilan sistem.

Program berikut memuat pola alokasi yang lengkap, yaitu buffer 256 byte yang dialokasikan, diisi, dibaca, lalu dibebaskan.

zig buffer-sensor.zig
const std = @import("std");

pub fn main() !void {
    const alloc = std.heap.page_allocator;

    const buffer = try alloc.alloc(u8, 256);
    defer alloc.free(buffer);

    const label = "sensor-ruang-utama";
    @memcpy(buffer[0..label.len], label);
    for (buffer[label.len..]) |*b| {
        b.* = 0;
    }

    std.debug.print("panjang buffer: {d} byte\n", .{buffer.len});
    std.debug.print("isi awal: {s}\n", .{buffer[0..label.len]});
}
text Keluaran
panjang buffer: 256 byte
isi awal: sensor-ruang-utama

Baris alloc(u8, 256) memperoleh buffer dan mengembalikan error bila memori habis, sehingga try meneruskannya keluar dari main. Label sensor disalin ke bagian awal buffer dengan @memcpy, kemudian sisa byte diisi 0 melalui capture |*b| yang menyediakan pointer ke setiap elemen. Baris defer alloc.free(buffer) menjadwalkan pembebasan pada akhir main, sehingga tidak ada jalur keluar yang melewatinya.

Argumen free adalah slice itu sendiri, bukan pointer telanjang, karena slice membawa panjang dan tipe yang diperlukan untuk mengembalikan memori dengan tepat. Setelah pembebasan berjalan, buffer tidak boleh dibaca maupun ditulis lagi. Ketentuan ini diperinci pada bagian kepasangan di bawah, beserta akibat bila pasangannya terlanggar.

bash
# mengompilasi dan menjalankan program
zig run buffer-sensor.zig
text Keluaran
panjang buffer: 256 byte
isi awal: sensor-ruang-utama

Konvensi parameter allocator#

Fungsi yang mengalokasikan memori menempatkan parameter alloc: std.mem.Allocator pada posisi pertama, dan nama alloc menjadi konvensi yang berlaku luas pada kode Zig. Penempatan ini membuat sifat alokatif sebuah fungsi terbaca dari tanda tangannya, tanpa perlu membaca tubuhnya. Pemanggil memilih penyedia memorinya dan meneruskannya secara tertulis, sehingga fungsi yang sama dapat bekerja dengan allocator mana pun.

zig Contoh: fungsi dengan parameter allocator
// tidak-dijalankan
fn siapkan(alloc: std.mem.Allocator, panjang: usize) ![]u8 {
    const buffer = try alloc.alloc(u8, panjang);
    @memset(buffer, 0);
    return buffer;
}
// hasilnya belum dibebaskan, sehingga nasibnya menunggu kontrak

Fungsi siapkan mengembalikan buffer yang belum dibebaskan, sehingga pertanyaan siapa yang wajib membebaskannya perlu jawaban yang tegas. Pertanyaan tersebut adalah inti disiplin kepemilikan, dan jawabannya dinyatakan sebagai kontrak pada dokumentasi fungsi. Untuk saat ini, cukup dicatat bahwa hasil siapkan tidak boleh dibiarkan tanpa pembebasan.

Keunggulan desain ini terasa ketika penyedia memori ditukar. Karena fungsi hanya mengenal antarmukanya, siapkan dapat menerima std.heap.page_allocator, allocator arena, maupun allocator pengujian tanpa perubahan satu baris pun di tubuhnya. Program dapat memilih strategi memori per komponen, dan pilihan itu tetap terlihat pada setiap titik panggilan.

Pasangan alloc dan free#

Setiap blok yang diperoleh dari sebuah allocator wajib dikembalikan ke allocator yang sama, tepat satu kali. Pasangan alloc dan free yang tidak lengkap atau tidak serasi menimbulkan dua kelas masalah, yaitu kebocoran bila pembebasan terlupa dan perilaku tidak terdefinisi bila memori dipakai atau dibebaskan dua kali. Zig tidak menegakkan pasangan ini pada waktu kompilasi, tetapi menyediakan pemeriksanya pada waktu pengujian.

AlokasiPembebasanKeterangan
alloc(T, n)free(slice)Blok baru sepanjang n elemen
dupe(T, data)free(slice)Salinan baru dari data yang ada
dupeSentinel(T, data, 0)free(slice)Salinan yang berakhir dengan nilai penanda
resize(slice, n)tetap free(slice)Mengubah panjang tanpa memindahkan data bila mungkin

Tabel di atas menunjukkan bahwa free selalu menerima slice hasil alokasi dengan tipe yang sama. Panjang yang tercantum pada slice dipakai untuk menghitung ukuran yang dikembalikan, sehingga slice yang panjangnya disesuaikan melalui resize tetap dibebaskan dengan benar. Pembebasan dengan allocator yang berbeda tidak dijamin bekerja, karena setiap strategi memegang pembukuan internalnya sendiri.

Kesalahan umum#

  1. free terlupa pada salah satu jalur keluar fungsi. Kebiasaan yang mencegahnya adalah menulis defer pembebasan tepat setelah alokasi berhasil, sebagaimana dilakukan pada buffer-sensor.zig.
  2. Blok dibebaskan dengan allocator yang berbeda dari yang mengalokasikan. Pembukuan kedua allocator tidak saling mengenal, dan akibatnya tidak terdefinisi. Simpan allocator dan bloknya pada cakupan yang sama.
  3. Slice dipakai setelah free. Baris pemakaiannya harus berada sebelum pembebasan, dan defer yang ditulis di awal membantu menegakkan urutan itu.

Ringkasan#

  • std.mem.Allocator adalah antarmuka penyedia memori dengan metode alloc, free, dupe, dupeSentinel, dan resize.
  • alloc mengembalikan error union yang membawa error.OutOfMemory, sehingga kegagalan alokasi ditangani secara eksplisit.
  • Fungsi yang mengalokasikan menerima parameter Allocator pada posisi pertama sesuai konvensi Zig.
  • Setiap alloc berpasangan dengan tepat satu free pada allocator yang sama, dan memori yang sudah dibebaskan tidak boleh disentuh lagi.

Referensi#