Deep EVM #7: Loop dan Kondisional Efisien Gas di Yul
Engineering Team
Loop di Yul vs Solidity
Solidity mengkompilasi loop for menjadi serangkaian opcode yang menyertakan pemeriksaan overflow, penanganan revert, dan boilerplate lainnya. Yul memungkinkan Anda menulis loop yang lebih ringkas dan efisien.
Solidity For Loop (Default)
for (uint256 i = 0; i < length; i++) {
// badan loop
}
Dikompilasi menjadi: inisialisasi + pemeriksaan overflow pada i++ + perbandingan + JUMPI + badan + increment + JUMP kembali.
Yul For Loop (Teroptimasi)
assembly {
for { let i := 0 } lt(i, length) { i := add(i, 1) } {
// badan loop
}
}
Perbedaan kunci: Yul tidak menyisipkan pemeriksaan overflow pada add(i, 1). Untuk counter loop yang tidak mungkin overflow (karena length < 2^256), ini menghemat ~30 gas per iterasi.
Teknik Optimasi Loop
1. Cache Batas Loop
// Buruk: membaca length dari storage setiap iterasi
assembly {
for { let i := 0 } lt(i, sload(lengthSlot)) { i := add(i, 1) } {
// SLOAD setiap iterasi = 100 gas warm per iterasi!
}
}
// Baik: cache length di stack
assembly {
let len := sload(lengthSlot) // Satu SLOAD
for { let i := 0 } lt(i, len) { i := add(i, 1) } {
// len dari stack = 3 gas (DUP)
}
}
Penghematan: (100 - 3) * jumlah_iterasi gas.
2. Hitung Mundur
assembly {
// Menghitung mundur menghemat gas karena perbandingan dengan 0 (ISZERO)
// lebih murah daripada perbandingan dengan nilai non-zero (LT)
let i := length
for { } gt(i, 0) { i := sub(i, 1) } {
let index := sub(i, 1) // index 0-based
// ... gunakan index
}
}
Penghematan: kecil (1-2 gas per iterasi) tetapi bertambah untuk loop panjang.
3. Loop Unrolling
Untuk loop dengan jumlah iterasi yang diketahui, unrolling menghilangkan overhead JUMP:
assembly {
// Daripada loop 4 iterasi:
// for { let i := 0 } lt(i, 4) { i := add(i, 1) } { ... }
// Unroll:
let s0 := sload(0)
let s1 := sload(1)
let s2 := sload(2)
let s3 := sload(3)
let total := add(add(s0, s1), add(s2, s3))
}
Menghemat: ~15 gas per iterasi yang di-unroll (JUMP + JUMPDEST + perbandingan + increment).
4. Batch Processing
assembly {
// Proses 32 byte sekaligus daripada 1 byte
let words := div(dataLength, 32)
for { let i := 0 } lt(i, words) { i := add(i, 1) } {
let word := mload(add(dataPtr, mul(i, 32)))
// Proses seluruh word 32-byte
}
// Handle sisa byte
let remaining := mod(dataLength, 32)
if remaining {
let lastWord := mload(add(dataPtr, mul(words, 32)))
// Mask byte yang relevan
}
}
Kondisional di Yul
If Statement
assembly {
if condition {
// dieksekusi jika condition != 0
}
// Yul TIDAK memiliki else!
}
Yul tidak memiliki else. Untuk logika if-else, gunakan switch:
Switch Statement
assembly {
switch condition
case 0 {
// false branch
}
default {
// true branch (non-zero)
}
}
Multi-Way Switch
assembly {
// Dispatch berdasarkan function selector
let selector := shr(224, calldataload(0))
switch selector
case 0xa9059cbb { // transfer(address,uint256)
// handle transfer
}
case 0x70a08231 { // balanceOf(address)
// handle balanceOf
}
case 0x095ea7b3 { // approve(address,uint256)
// handle approve
}
default {
revert(0, 0) // fungsi tidak dikenali
}
}
Short-Circuit Evaluation
Solidity mengkompilasi && dan || dengan branching terpisah. Di Yul, Anda bisa menggunakan operasi bitwise untuk evaluasi tanpa branching:
assembly {
// Solidity: require(a > 0 && b > 0 && c > 0)
// Menghasilkan 3 JUMPI
// Yul: satu pemeriksaan gabungan
if iszero(and(and(gt(a, 0), gt(b, 0)), gt(c, 0))) {
revert(0, 0)
}
// Hanya 1 JUMPI!
}
Penghematan: ~20 gas per kondisi tambahan yang digabungkan.
Perbandingan Gas: Yul vs Solidity
| Operasi | Solidity | Yul | Penghematan |
|---|---|---|---|
| Loop increment (i++) | ~35 gas | ~6 gas | ~29 gas |
| Pemeriksaan 3 kondisi | ~45 gas | ~25 gas | ~20 gas |
| Array sum (10 elemen) | ~800 gas | ~550 gas | ~250 gas |
| Storage cache + loop | bervariasi | -30% | signifikan |
Pola Tingkat Lanjut
Early Exit dari Loop
assembly {
let found := 0
for { let i := 0 } lt(i, length) { i := add(i, 1) } {
let val := sload(add(baseSlot, i))
if eq(val, target) {
found := 1
i := length // Paksa keluar dari loop
}
}
}
Loop dengan Akumulasi Memory
assembly {
let ptr := mload(0x40) // Alokasi output
let outPtr := ptr
for { let i := 0 } lt(i, count) { i := add(i, 1) } {
let val := sload(add(baseSlot, i))
if gt(val, threshold) {
mstore(outPtr, val)
outPtr := add(outPtr, 0x20)
}
}
// Update FMP
mstore(0x40, outPtr)
// Jumlah hasil = (outPtr - ptr) / 32
}
Perangkap Umum
- Infinite loop — Tanpa batas gas, loop Yul yang salah menghabiskan semua gas. Selalu pastikan kondisi terminasi.
- Off-by-one — Tanpa pemeriksaan batas otomatis, sangat mudah membaca melewati akhir array.
- Stack depth — Loop kompleks bisa mendorong stack mendekati batas 16 variabel lokal. Pindahkan data ke memory jika diperlukan.
Kesimpulan
Loop dan kondisional di Yul memberikan kontrol penuh atas biaya gas. Cache batas loop, gabungkan kondisi, gunakan loop unrolling untuk iterasi yang diketahui, dan proses data dalam batch word 32-byte. Teknik-teknik ini secara konsisten menghasilkan penghematan 20-40% dibanding Solidity murni.