Deep EVM #7: Gaseffiziente Schleifen und Verzweigungen in Yul
Engineering Team
Schleifen in der EVM sind teuer
Jede Iteration einer Schleife in der EVM kostet Gas fuer JUMPI (10 Gas), den Vergleich (3 Gas), die Inkrementierung (3+3 Gas fuer ADD+MSTORE/DUP) und die eigentliche Arbeit. Bei Tausenden von Iterationen summiert sich das schnell.
In Solidity generiert der Compiler relativ effizienten Schleifencode, aber er kann die Semantik Ihrer Schleife nicht aendern. In Yul haben Sie die volle Kontrolle.
Grundlegende Schleifenoptimierung
Rueckwaerts zaehlen statt vorwaerts
// Vorwaerts (standard):
for { let i := 0 } lt(i, n) { i := add(i, 1) } {
// LT-Vergleich: 3 Gas pro Iteration
}
// Rueckwaerts (optimiert):
for { let i := n } i { i := sub(i, 1) } {
// ISZERO implizit im Bedingungstest: spart 1 Opcode
}
Warum ist Rueckwaertszaehlen schneller? Der Bedingungstest i ist aequivalent zu iszero(iszero(i)), was der EVM-Compiler zu einem einfachen Nulltest optimiert. lt(i, n) erfordert einen expliziten LT-Opcode.
Loop-Unrolling
// Statt 4 Iterationen:
for { let i := 0 } lt(i, 4) { i := add(i, 1) } {
sstore(add(baseSlot, i), 0)
}
// Entrollt (spart JUMP/JUMPI-Overhead):
sstore(baseSlot, 0)
sstore(add(baseSlot, 1), 0)
sstore(add(baseSlot, 2), 0)
sstore(add(baseSlot, 3), 0)
Bei kleinen, bekannten Iterationszahlen spart Unrolling die JUMP/JUMPI-Kosten (10 Gas pro Iteration) und den Schleifenoverhead.
Verzweigungen optimieren
Switch statt if-else-Ketten
// Schlecht: if-else-Kette (O(n) Vergleiche)
if eq(selector, 0xa9059cbb) { /* transfer */ }
if eq(selector, 0x70a08231) { /* balanceOf */ }
if eq(selector, 0x095ea7b3) { /* approve */ }
// Besser: switch-Anweisung
switch selector
case 0xa9059cbb { /* transfer */ }
case 0x70a08231 { /* balanceOf */ }
case 0x095ea7b3 { /* approve */ }
default { revert(0, 0) }
Der Yul-Compiler optimiert switch-Anweisungen besser als verschachtelte if-Anweisungen, obwohl auf EVM-Ebene beides zu Vergleichen und JUMPIs kompiliert.
Batch-Verarbeitung
Mehrere Storage-Writes buendeln
// Statt einzelner Schreibvorgaenge:
function batchWrite(slot, values_ptr, count) {
for { let i := 0 } lt(i, count) { i := add(i, 1) } {
let value := mload(add(values_ptr, mul(i, 32)))
sstore(add(slot, i), value)
}
}
Parallele Vergleiche mit Bitmasks
// Pruefe mehrere Bedingungen gleichzeitig:
let flags := or(
mul(eq(a, target_a), 1), // Bit 0
mul(eq(b, target_b), 2) // Bit 1
)
if eq(flags, 3) {
// Beide Bedingungen erfuellt
}
Gasvergleich: Solidity vs. Yul
Ein konkretes Beispiel — Summierung eines Arrays:
// Solidity: ~250 Gas fuer 10 Elemente
function sumSolidity(uint256[] calldata arr) external pure returns (uint256 total) {
for (uint256 i = 0; i < arr.length; i++) {
total += arr[i];
}
}
// Yul: ~180 Gas fuer 10 Elemente
function sumYul(uint256[] calldata arr) external pure returns (uint256 total) {
assembly {
let len := arr.length
let ptr := arr.offset
for { let i := 0 } lt(i, len) { i := add(i, 1) } {
total := add(total, calldataload(add(ptr, mul(i, 32))))
}
}
}
Die Yul-Version spart ~30% Gas, hauptsaechlich durch Vermeidung von Soliditys Bounds-Checking und Stack-Management-Overhead.
Fortgeschrittene Techniken
Branchless Minimum/Maximum
// Minimum ohne Verzweigung (spart JUMPI):
// min(a, b) = b ^ ((a ^ b) & -(a < b))
function branchlessMin(a, b) -> result {
let diff := xor(a, b)
let mask := sub(0, lt(a, b)) // 0xfff...fff wenn a < b, sonst 0
result := xor(b, and(diff, mask))
}
Effiziente Modulo-Operationen fuer Zweierpotenzen
// x % 8 (teuer):
let r := mod(x, 8) // 5 Gas
// x & 7 (guenstig):
let r := and(x, 7) // 3 Gas
Fazit
Gaseffiziente Schleifen und Verzweigungen in Yul erfordern ein Verstaendnis der zugrunde liegenden Opcode-Kosten. Rueckwaerts zaehlen, Loop-Unrolling, Batch-Verarbeitung und Branchless-Techniken koennen den Gasverbrauch erheblich reduzieren. Im naechsten und letzten Artikel dieser Serie werden wir einen vollstaendigen Token-Swap in reinem Yul implementieren und alle bisher gelernten Techniken anwenden.