Deep EVM #7 : Boucles et conditionnels efficaces en gas dans Yul
Engineering Team
Les boucles en Yul
Yul offre un seul type de boucle : for. Mais cette boucle est suffisamment flexible pour exprimer des patterns while, do-while et de parcours de tableaux.
Syntaxe de base
for { let i := 0 } lt(i, 10) { i := add(i, 1) } {
// Corps de la boucle
}
La structure est for { init } condition { post } { body }. Chaque partie compile directement en opcodes JUMP/JUMPI — il n’y a pas d’abstraction intermédiaire.
Boucle while
for { } condition { } {
// Corps — exécuté tant que condition est vraie
}
Incrémentation en pré vs post
En Yul, l’incrément est toujours explicite avec add. Il n’y a pas de i++ — vous écrivez i := add(i, 1). Cela coûte exactement PUSH1 + ADD + SWAP + POP = 12 gas par itération.
Comparaison avec Solidity (mode unchecked) :
// Solidity unchecked — compile en un code similaire
unchecked {
for (uint256 i; i < 10; ++i) { ... }
}
Le Yul produit un bytecode presque identique au unchecked Solidity. L’avantage de Yul est le contrôle total : vous pouvez structurer la boucle pour minimiser les opérations de pile.
Les conditionnels en Yul
if
Yul n’a pas de else. Si vous avez besoin de branches multiples, utilisez switch ou enchaînez des if :
if iszero(value) {
revert(0, 0)
}
switch
switch selector
case 0x70a08231 {
// balanceOf
}
case 0xa9059cbb {
// transfer
}
default {
revert(0, 0)
}
Le switch en Yul compile en une chaîne d’EQ + JUMPI — similaire au dispatcher if-else de Solidity. Ce n’est pas une table de saut O(1) ; c’est du O(N) dans le pire cas.
Optimisations pratiques
Déroulage de boucle
Pour les boucles courtes avec un nombre fixe d’itérations, déroulez manuellement :
// Au lieu de boucler 4 fois :
mstore(add(ptr, 0x00), val0)
mstore(add(ptr, 0x20), val1)
mstore(add(ptr, 0x40), val2)
mstore(add(ptr, 0x60), val3)
Économise le coût de la condition et de l’incrément à chaque itération (environ 18 gas par itération).
Compteur décroissant
// Plus efficace : compter vers le bas
for { let i := n } gt(i, 0) { i := sub(i, 1) } {
// Le test `gt(i, 0)` est 1 gas moins cher que `lt(i, n)` car
// il n'a pas besoin de charger n depuis la pile
}
Arrêt anticipé
Sortez de la boucle dès que possible avec break :
for { let i := 0 } lt(i, length) { i := add(i, 1) } {
let element := mload(add(ptr, mul(i, 0x20)))
if eq(element, target) {
result := i
break
}
}
Benchmarks de gas
| Pattern | Gas par itération (Solidity) | Gas par itération (Yul) | Économie |
|---|---|---|---|
| Boucle standard | ~95 | ~78 | 18 % |
| Avec unchecked | ~80 | ~78 | 3 % |
| Accès tableau | ~110 | ~85 | 23 % |
| Copie mémoire | ~120 | ~90 | 25 % |
Les économies sont les plus significatives pour les boucles qui accèdent à la mémoire ou au stockage, où l’overhead de Solidity (vérifications de limites, ABI encoding) est le plus élevé.
Conclusion
Les boucles et conditionnels en Yul offrent un contrôle fin sur le gas. Les gains les plus importants viennent de l’élimination des vérifications de limites de Solidity, du déroulage des boucles courtes et de l’utilisation de compteurs décroissants.
Dans le prochain article, nous mettrons en pratique toutes ces techniques en construisant un échange de tokens en Yul pur.