ArenaAllocator

Mengelola masa hidup terlingkup dengan ArenaAllocator yang dibebaskan dalam satu panggilan.

Daftar isi

Sebagian alokasi memori mempunyai masa hidup yang sama persis, misalnya sekumpulan data sementara untuk menyusun satu laporan. Membebaskannya satu per satu menambah pembukuan yang rawan terlewat, sedangkan ArenaAllocator menawarkan jalan lain, yaitu membebaskan semuanya dalam satu panggilan. Topik ini membahas cara kerja arena, kapan arena menjadi pilihan yang tepat, serta dua program yang menunjukkan penghematan pembebasannya.

Konsep#

Arena dan masa hidup terlingkup#

ArenaAllocator adalah pembungkus di atas allocator lain, yang disebut allocator pendukung, dan seluruh alokasi melalui arena tunduk pada satu masa hidup. Arena dibuat dengan .init(pendukung), menyediakan allocator anaknya melalui arena.allocator(), lalu menutup masa hidup seluruh bloknya dengan satu arena.deinit(). Di dalam rentang itu, pemanggilan alloc dan dupe berjalan persis seperti pada allocator biasa.

zig tiga-alokasi.zig
const std = @import("std");

pub fn main() !void {
    var arena: std.heap.ArenaAllocator = .init(std.heap.page_allocator);
    defer arena.deinit();

    const alloc = arena.allocator();

    const label = try alloc.dupe(u8, "rekap-harian");
    const angka = try alloc.alloc(u32, 64);
    const catatan = try alloc.dupe(u8, "sensor aktif");

    angka[0] = 42;

    std.debug.print("label: {s}\n", .{label});
    std.debug.print("catatan: {s}\n", .{catatan});
    std.debug.print("angka[0]: {d}\n", .{angka[0]});
    std.debug.print("semua alokasi di atas dibebaskan oleh satu arena.deinit()\n", .{});
}
text Keluaran
label: rekap-harian
catatan: sensor aktif
angka[0]: 42
semua alokasi di atas dibebaskan oleh satu arena.deinit()

Tiga alokasi terjadi di dalam rentang arena, yaitu label, angka, dan catatan, dan tidak satu pun dari ketiganya dibebaskan secara tunggal. Satu-satunya pembebasan adalah defer arena.deinit() yang terdaftar sejak arena dibuat. Ketika main berakhir, pembebasan itu berjalan dan ketiga blok kembali sekaligus ke allocator pendukungnya, sehingga jumlah panggilan pembebasan tidak lagi bergantung pada jumlah alokasi.

bash
# mengompilasi dan menjalankan program
zig run tiga-alokasi.zig
text Keluaran
label: rekap-harian
catatan: sensor aktif
angka[0]: 42
semua alokasi di atas dibebaskan oleh satu arena.deinit()

Cara arena mengelola blok#

Di balik antarmukanya, arena memperoleh blok besar dari allocator pendukung lalu memotongnya untuk setiap permintaan alokasi. Permintaan kecil tidak lagi sampai ke sistem operasi, karena cukup dilayani dari sisa blok besar tersebut. Metode free yang dipanggil pada allocator arena praktis tidak melakukan apa-apa, karena potongan yang dikembalikan tidak dapat dipakai ulang per potongan.

Ketika deinit dipanggil, arena melepaskan seluruh blok besar yang pernah dimintanya kepada allocator pendukung, dan semua potongan ikut kembali bersama bloknya. Diagram berikut merangkum perjalanan tersebut dari sisi masa hidup.

Selama cakupan aktifArenaAllocatorblok labelblok angkablok catatantiga alokasi menumpuk, tanpa pemanggilan freearena.deinit()Setelah deinittidak ada blok tersisaseluruhnya kembali ke allocator pendukung
ArenaAllocator menampung tiga alokasi dan membebaskannya sekaligus pada satu deinit

Model ini menukar kendali dengan kesederhanaan. Satu blok di tengah jalan tidak dapat dibebaskan lebih awal, sehingga arena hanya cocok untuk data yang memang seumur hidup dengan cakupannya. Untuk pola itu, penghematannya nyata, yaitu pembukuan free per blok hilang seluruhnya, dan bersamanya hilang pula peluang satu pembebasan terlewat.

Contoh Lengkap#

Kapan arena menjadi pilihan#

Arena tepat ketika masa hidup datanya jatuh pada satu cakupan yang jelas. Contoh yang lazim adalah membangun struktur hasil parsing dari satu masukan, memproses satu permintaan dari awal sampai responsnya selesai, dan menyiapkan perlengkapan sementara untuk satu pengujian. Setelah cakupan itu selesai, seluruh datanya tidak dibutuhkan lagi, dan tepat di titik itulah satu deinit menutup semuanya.

Program berikut menyusun rekap dari tiga catatan sensor. Fungsi pembentuknya mengalokasikan beberapa kali tanpa satu pun pembebasan, karena seluruh bloknya tunduk pada arena yang dikelola pemanggil.

zig baris-rekap.zig
const std = @import("std");

fn bangun_rekap(alloc: std.mem.Allocator) ![][]u8 {
    const sumber = [_][]const u8{ "suhu=24", "lembap=41", "tekanan=1012" };

    var baris: std.ArrayList([]u8) = .empty;
    for (sumber) |teks| {
        try baris.append(alloc, try alloc.dupe(u8, teks));
    }
    return try baris.toOwnedSlice(alloc);
}

pub fn main() !void {
    var arena: std.heap.ArenaAllocator = .init(std.heap.page_allocator);
    defer arena.deinit();

    const rekap = try bangun_rekap(arena.allocator());
    for (rekap) |b| {
        std.debug.print("{s}\n", .{b});
    }
    std.debug.print("{d} baris dibebaskan sekaligus oleh arena\n", .{rekap.len});
}
text Keluaran
suhu=24
lembap=41
tekanan=1012
3 baris dibebaskan sekaligus oleh arena

Fungsi bangun_rekap mengalokasikan empat kali, yaitu slice penampung dan tiga salinan teks, tanpa satu pun free di tubuhnya. Pemanggilnya meneruskan allocator arena, sehingga seluruh blok tersebut tunduk pada satu arena.deinit() yang sudah terjadwal di main. Bila pola yang sama ditulis dengan allocator biasa, empat panggilan free harus ditempatkan dengan benar, dan risikonya bertambah seiring bertambahnya jumlah baris.

bash
# mengompilasi dan menjalankan program
zig run baris-rekap.zig
text Keluaran
suhu=24
lembap=41
tekanan=1012
3 baris dibebaskan sekaligus oleh arena

Arena juga dapat bertingkat, yaitu sebuah arena dibangun di atas arena lain, ketika cakupan kecil hidup di dalam cakupan besar. Pembebasan tetap bergerak dari dalam ke luar, karena arena dalam harus deinit lebih dahulu daripada arena luarnya. Pola bertingkat ini jarang dibutuhkan pada program kecil, tetapi berguna pada pemrosesan bertahap yang cakupannya bersarang.

Arena juga menjadi alat pengukuran yang jujur. Karena seluruh alokasi sementara tunduk pada satu cakupan, puncak pemakaian memori satu permintaan dapat diperkirakan dari besaran blok yang diminta arena, dan lonjakan pemakaian setiap permintaan langsung terlihat pada jumlah bloknya. Keterbacaan ini sulit diperoleh pada alokasi yang tersebar di banyak pemilik, sehingga tim yang menilai jejak memori per permintaan sering memulainya dengan mengalihkan data sementara ke arena.

Kesalahan umum#

  1. Allocator arena dipakai setelah deinit. Alokasi berikutnya menulis pada memori yang tidak sah, dan pelanggarannya dapat luput dari pemeriksaan. Pastikan arena hidup lebih panjang daripada seluruh pemakaiannya.
  2. Data yang harus hidup lebih lama dialokasikan pada arena yang pendek usianya. Saat arena ditutup, data itu ikut terhapus tanpa pesan apa pun. Pisahkan arena per cakupan, atau alihkan data jangka panjang ke allocator yang masa hidupnya sepadan.
  3. Arena disalin atau dipindahkan setelah arena.allocator() diambil. Allocator anak menyimpan alamat arena aslinya, sehingga salinan tersebut menggantung pada lokasi yang salah. Deklarasikan arena satu kali dan biarkan berada di tempatnya selama dipakai.

Ringkasan#

  • ArenaAllocator membungkus allocator pendukung dan membebaskan seluruh alokasi anaknya dalam satu arena.deinit().
  • Arena memotong blok besar untuk setiap permintaan, sehingga free per potongan praktis tidak berpengaruh dan tidak diperlukan.
  • Arena tepat untuk data yang seumur hidup dengan satu cakupan, misalnya hasil parsing atau pemrosesan satu permintaan.
  • Ketiadaan pembebasan per blok menuntut disiplin menentukan data mana yang boleh masuk arena.

Referensi#