Memory Management di Wasm — WebAssembly

Memory Management di WebAssembly WebAssembly menggunakan linear memory — satu blok kontinu dari bytes yang bisa diakses dari Wasm dan JavaScript. Tidak ada…

Memori Wasm (linear memory) adalah satu deretan byte panjang yang dibaca dan ditulis lewat alamat: byte ke-0, ke-1, dan seterusnya. Dia cuma bisa bertambah panjang, per 65.536 byte, dan gak ada garbage collector yang membersihkannya. Siapa yang boleh memakai bagian mana dicatat oleh allocator. Bayangin gudang Homestay Kabut: satu rak panjang dengan slot bernomor. Raknya bisa disambung per bagian, tapi gak pernah dipotong lagi. Bu Sri mencatat di buku slot mana yang dipakai tamu siapa. Kalau barang tamu gak pernah diambil dan catatannya gak dicoret, slotnya terpakai selamanya. Dan denah gudang lama gak berlaku lagi setiap kali rak disambung. Raknya linear memory, satu bagiannya satu halaman (page), buku catatannya allocator, barang yang gak diambil itu memory leak, dan denah lama itu view JavaScript yang jadi detached.

🧪 Tebakan cepat: foto Kamar Teras 1200 × 800 RGBA butuh 3.840.000 byte. Satu halaman memori Wasm 65.536 byte. Butuh berapa halaman: 58, 59, atau 60? Tebak dulu, jawabannya ada di bagian "Halaman 64 KiB dan memory.grow".

Linear memory: satu deretan byte

Pelajaran ini memakai memori.wat (269 byte setelah wat2wasm 1.0.42). Memorinya dideklarasikan begini:

(memory (export "memory") 1 256) ;; mulai 1 halaman (64 KiB), paling banyak 256 (16 MiB)

Dari JavaScript, memorinya terlihat sebagai satu ArrayBuffer, memory.buffer. Untuk membaca dan menulisnya, buat view:

const { memory } = instance.exports;
const bytes = new Uint8Array(memory.buffer); // per byte
const ints = new Int32Array(memory.buffer);  // per 4 byte, di atas byte yang sama
ints[0] = 1000;
console.log(bytes[0], bytes[1], bytes[2], bytes[3]); // 232 3 0 0

Halaman 64 KiB dan memory.grow

Memori Wasm diukur per halaman: 1 halaman = 65.536 byte (64 KiB). Foto Kamar Teras 3.840.000 byte, dibagi 65.536 = 58,59. Halaman gak bisa dipotong, jadi dibulatkan ke atas: 59 halaman (3.866.624 byte). Itu jawaban tebakannya.

View lama jadi detached

Setiap kali memori tumbuh, memory.buffer diganti ArrayBuffer baru, dan yang lama di-detach: panjangnya jadi 0. View yang dibuat sebelumnya ikut kosong.

let lihat = new Uint8Array(memory.buffer); // 65.536 byte
const ptr = pesan(3840000);                // di dalam Wasm memori tumbuh 1 -> 59 halaman
console.log(lihat.byteLength, lihat[0]);   // 0 undefined
lihat.set(foto, ptr);                      // TypeError
lihat = new Uint8Array(memory.buffer);     // view baru, 3.866.624 byte
lihat.set(foto, ptr);                      // berhasil

Bump allocator buatan tangan

Allocator paling sederhana menyimpan satu angka: alamat pertama yang belum dipakai. Memesan berarti memberi alamat itu, lalu menggesernya maju. Ini intinya di memori.wat:

(global $atas (mut i32) (i32.const 1024)) ;; 1.024 byte pertama dibiarkan kosong

(func (export "pesan") (param $n i32) (result i32)
  (local $ptr i32) (local $akhir i32)
  global.get $atas
  local.tee $ptr          ;; alamat yang akan diberikan
  local.get $n
  i32.add
  local.set $akhir        ;; $ptr + $n
  local.get $akhir
  memory.size
  i32.const 16
  i32.shl                 ;; ukuran memori dalam byte: halaman × 65.536
  i32.gt_u
  if                      ;; gak cukup? tambah halaman, dibulatkan ke atas
    local.get $akhir
    memory.size
    i32.const 16
    i32.shl
    i32.sub
    i32.const 65535
    i32.add
    i32.const 16
    i32.shr_u
    memory.grow
    i32.const -1
    i32.eq
    if
      i32.const 0         ;; melewati 256 halaman: kembalikan 0
      return
    end
  end
  local.get $akhir
  global.set $atas
  local.get $ptr)

(func (export "kosongkan") ;; lepas semuanya sekaligus
  i32.const 1024
  global.set $atas)

Lima kali pesan(3840000), lalu kosongkan(), hasilnya di browser:

Panggilan Hasil $atas sesudahnya Halaman
pesan ke-1 1024 3.841.024 59
pesan ke-2 3841024 7.681.024 118
pesan ke-3 7681024 11.521.024 176
pesan ke-4 11521024 15.361.024 235
pesan ke-5 0 (gagal) 15.361.024 235
kosongkan() 1.024 235

malloc dan free di C, allocator di Rust

Allocator sungguhan menyimpan daftar blok kosong, jadi blok yang dilepas bisa dipakai lagi.

Batas memori

📌 Memory64

Memory64 memakai alamat i64, jadi bisa lebih dari 4 GiB. Fiturnya sudah masuk standar (Wasm 3.0) dan jalan di Chrome 133, Firefox 134, dan Node 24. Safari baru punya di balik flag: WebKit 26.4 yang dipakai di sini menolak modulnya dengan CompileError: WebAssembly.Module doesn't parse at byte 24: Memory64 is not enabled. Di Chromium 147, memori 64-bit dibatasi 262.144 halaman (16 GiB). Alamatnya i64, jadi dari JavaScript dikirim sebagai BigInt. Pakai hanya kalau datanya memang lebih dari 4 GiB.

⚠️ Jebakan yang sering ketemu

TL;DR: Linear memory adalah satu ArrayBuffer (memory.buffer) yang dibaca lewat view seperti Uint8Array dan Int32Array (little-endian), diukur per halaman 65.536 byte: foto 3.840.000 byte butuh 59 halaman. memory.grow menambah halaman (Wasm memberi -1, JS melempar RangeError kalau lewat maximum), memori gak pernah mengecil, dan tiap grow men-detach view lama (byteLength 0, set jadi TypeError), jadi buat view baru. Gak ada garbage collector untuk linear memory: allocator yang mencatat. Bump allocator cuma menggeser $atas maju dan melepas semua sekaligus; malloc/free di C dan dlmalloc di Rust bisa memakai ulang blok yang dilepas. Lupa _free atau .free() = leak (10 Foto tanpa .free(): 17 jadi 607 halaman). Batas wasm32 65.536 halaman = 4 GiB; Memory64 melewatinya di Chrome 133, Firefox 134, Node 24, belum di Safari tanpa flag. Jangan pakai wee_alloc.

Yang akan kamu pelajari