Deep EVM #6: Manajemen Memory Yul — mstore, mload, dan Free Memory Pointer
Engineering Team
Memory di EVM: Model Fundamental
Memory EVM adalah array byte linear yang dimulai dari nol dan tumbuh sesuai kebutuhan. Tidak seperti storage yang persisten, memory hanya ada selama panggilan fungsi saat ini — ia dibersihkan sepenuhnya ketika eksekusi selesai atau sub-panggilan dimulai.
Dua opcode fundamental untuk memory:
- MSTORE(offset, value) — Menulis 32 byte di offset yang diberikan
- MLOAD(offset) — Membaca 32 byte dari offset yang diberikan
assembly {
mstore(0x00, 0x42) // Tulis 0x42 di memory[0x00..0x1f]
let val := mload(0x00) // Baca 32 byte dari memory[0x00]
// val = 0x42
}
MSTORE8: Menulis Satu Byte
assembly {
mstore8(0x00, 0xff) // Tulis 1 byte: memory[0x00] = 0xff
// Byte lain di word ini tidak terpengaruh
}
Free Memory Pointer Solidity
Solidity menyimpan pointer ke memory bebas di offset 0x40. Setiap alokasi memory memperbarui pointer ini:
assembly {
let fmp := mload(0x40) // Baca pointer saat ini
// Alokasi 64 byte
let ptr := fmp
mstore(0x40, add(fmp, 0x40)) // Update pointer
// Gunakan ptr..ptr+64
mstore(ptr, 0x01) // Tulis di blok yang dialokasi
mstore(add(ptr, 0x20), 0x02)
}
PENTING: Jika Anda menggunakan inline assembly dan mengabaikan free memory pointer, Anda bisa menimpa data yang dialokasi oleh Solidity. Ini menyebabkan bug yang sangat sulit di-debug.
Tata Letak Memory Solidity
Offset Ukuran Tujuan
0x00-0x1f 32 byte Scratch space 1 (untuk hashing)
0x20-0x3f 32 byte Scratch space 2 (untuk hashing)
0x40-0x5f 32 byte Free memory pointer
0x60-0x7f 32 byte Slot zero (selalu 0)
0x80+ dinamis Area alokasi
Scratch space (0x00-0x3f) aman untuk digunakan dalam assembly karena Solidity hanya menggunakannya sementara untuk operasi keccak256.
Pola Memory Umum
Pola 1: Hashing Dua Nilai
assembly {
// keccak256(abi.encodePacked(a, b))
mstore(0x00, a)
mstore(0x20, b)
let hash := keccak256(0x00, 0x40) // Hash 64 byte
}
Ini adalah cara paling efisien untuk hashing — menggunakan scratch space tanpa alokasi memory.
Pola 2: Encoding ABI Manual
assembly {
let ptr := mload(0x40) // Alokasi dari free memory pointer
// Encode: transfer(address,uint256)
mstore(ptr, 0xa9059cbb00000000000000000000000000000000000000000000000000000000)
mstore(add(ptr, 0x04), to) // address (padded ke 32 byte)
mstore(add(ptr, 0x24), amount) // uint256
// Call
let ok := call(gas(), token, 0, ptr, 0x44, 0x00, 0x20)
}
Pola 3: Membaca Return Data
assembly {
// Setelah call, baca return data
let size := returndatasize()
let ptr := mload(0x40)
returndatacopy(ptr, 0, size) // Salin return data ke memory
// Parse hasil
let success := mload(ptr) // Asumsi: mengembalikan bool
}
Biaya Ekspansi Memory
Memory tidak gratis — biayanya tumbuh secara kuadratik:
biaya(ukuran_word) = 3 * ukuran_word + floor(ukuran_word^2 / 512)
| Memory Digunakan | Biaya Total | Biaya Marginal |
|---|---|---|
| 32 byte (1 word) | 3 gas | 3 gas |
| 1 KB (32 words) | 98 gas | 3 gas/word |
| 10 KB (320 words) | 1160 gas | 4+ gas/word |
| 100 KB (3200 words) | 29600 gas | 16+ gas/word |
| 1 MB (32000 words) | 2048000+ gas | mahal |
Biaya kuadratik ini berarti Anda harus sangat berhati-hati dengan penggunaan memory. Jangan pernah mengalokasi lebih dari yang diperlukan.
Teknik Optimasi Memory
1. Gunakan Kembali Memory
Daripada terus mengalokasi, tulis ulang area memory yang sama:
assembly {
// Daripada alokasi baru untuk setiap operasi:
let scratch := 0x00 // Gunakan scratch space
// Operasi 1
mstore(scratch, data1)
let hash1 := keccak256(scratch, 0x20)
// Operasi 2 — tulis ulang area yang sama
mstore(scratch, data2)
let hash2 := keccak256(scratch, 0x20)
}
2. Hindari Free Memory Pointer untuk Operasi Sementara
assembly {
// Buruk: alokasi melalui FMP untuk data sementara
let ptr := mload(0x40)
mstore(0x40, add(ptr, 0x20))
mstore(ptr, someValue)
// Memory ini tidak pernah dibebaskan
// Baik: gunakan scratch space
mstore(0x00, someValue)
// Tidak ada alokasi permanen
}
3. Batch Memory Write
assembly {
// Tulis beberapa nilai secara berurutan
let base := mload(0x40)
mstore(base, val1)
mstore(add(base, 0x20), val2)
mstore(add(base, 0x40), val3)
mstore(add(base, 0x60), val4)
// Satu update FMP
mstore(0x40, add(base, 0x80))
}
MCOPY (EIP-5656)
Diperkenalkan di upgrade Cancun (2024), MCOPY menyalin byte dalam memory:
assembly {
mcopy(dst, src, length)
}
Biaya: 3 gas + 3 gas per word yang disalin + biaya ekspansi. Ini lebih murah daripada loop MLOAD/MSTORE manual untuk blok data besar.
Memory dalam Konteks Sub-panggilan
Ketika kontrak memanggil kontrak lain (CALL, STATICCALL, DELEGATECALL), kontrak yang dipanggil mendapatkan memory baru yang kosong. Memory pemanggil dipertahankan tetapi tidak bisa diakses oleh yang dipanggil.
Data dikomunikasikan antara pemanggil dan yang dipanggil melalui:
- Calldata (input): Pemanggil menulis ke memory, meneruskan via parameter offset/length ke CALL
- Return data (output): Yang dipanggil menulis ke memory-nya, pemanggil membaca via RETURNDATACOPY
Kesimpulan
Manajemen memory yang efisien adalah keterampilan kritis untuk pengembang Yul. Pahami tata letak memory Solidity, gunakan scratch space untuk operasi sementara, waspadai biaya ekspansi kuadratik, dan selalu pertahankan konsistensi free memory pointer saat mencampur Solidity dan inline assembly.