[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"article-deep-evm-7-gas-efficient-loops-conditionals-yul":3},{"article":4,"author":58},{"id":5,"category_id":6,"title":7,"slug":8,"excerpt":9,"content_md":10,"content_html":11,"locale":12,"author_id":13,"published":14,"published_at":15,"meta_title":7,"meta_description":16,"focus_keyword":17,"og_image":18,"canonical_url":18,"robots_meta":19,"created_at":15,"updated_at":15,"tags":20,"category_name":38,"related_articles":39},"d0000000-0000-0000-0000-000000000107","a0000000-0000-0000-0000-000000000002","Deep EVM #7: Gas-Efficient Loops and Conditionals in Yul","deep-evm-7-gas-efficient-loops-conditionals-yul","Optimize EVM loops and conditionals in Yul: for-loop gas anatomy, switch vs if, unchecked arithmetic, loop unrolling, and benchmarks comparing Solidity and Yul iteration patterns.","## Why Loops Are Where Gas Goes to Die\n\nIn most smart contracts, the single largest gas consumer is iteration. A function that loops over an array of 100 elements executes its body 100 times — and every opcode inside that body is multiplied by 100. A single unnecessary SLOAD inside a loop costs 10,000-210,000 gas. An unoptimized loop counter adds 4,000 gas over 100 iterations.\n\nThis article breaks down the exact gas cost of loop constructs in Yul and Solidity, and shows patterns that cut loop gas by 30-60%.\n\n## Anatomy of a Yul For-Loop\n\n```yul\nfor { let i := 0 } lt(i, 10) { i := add(i, 1) } {\n    \u002F\u002F body\n}\n```\n\nThis compiles to the following opcode sequence:\n\n```\n\u002F\u002F Initialization: let i := 0\nPUSH1 0x00          \u002F\u002F 3 gas (executed once)\n\n\u002F\u002F Condition check: lt(i, 10)\nJUMPDEST            \u002F\u002F 1 gas (loop entry point)\nDUP1                \u002F\u002F 3 gas\nPUSH1 0x0a          \u002F\u002F 3 gas\nLT                  \u002F\u002F 3 gas\nPUSH2 \u003Cexit>        \u002F\u002F 3 gas\nJUMPI               \u002F\u002F 10 gas (conditional jump)\n\n\u002F\u002F Body: (varies)\n...\n\n\u002F\u002F Post-iteration: i := add(i, 1)\nPUSH1 0x01          \u002F\u002F 3 gas\nADD                 \u002F\u002F 3 gas\n\n\u002F\u002F Jump back to condition\nPUSH2 \u003Cloop_start>  \u002F\u002F 3 gas\nJUMP                \u002F\u002F 8 gas\n```\n\nPer iteration, the loop overhead is:\n- Condition check: 1 + 3 + 3 + 3 + 3 + 10 = **23 gas**\n- Post-iteration: 3 + 3 = **6 gas**\n- Jump back: 3 + 8 = **11 gas**\n- **Total overhead per iteration: 40 gas**\n\nFor 100 iterations, that is 4,000 gas just in loop mechanics — before any work in the body.\n\n## Solidity Loop vs Yul Loop: A Benchmark\n\nLet us compare summing a `uint256[] calldata` array:\n\n### Solidity (Checked Arithmetic)\n```solidity\nfunction sumSolidity(uint256[] calldata arr) external pure returns (uint256 total) {\n    for (uint256 i = 0; i \u003C arr.length; i++) {\n        total += arr[i];\n    }\n}\n```\n\nGas per iteration:\n- Loop overhead: ~60 gas (Solidity adds overflow checks on `i++` and `total +=`)\n- CALLDATALOAD: 3 gas\n- Offset computation: ~10 gas\n- **Total: ~73 gas\u002Fiteration**\n\n### Solidity (Unchecked)\n```solidity\nfunction sumUnchecked(uint256[] calldata arr) external pure returns (uint256 total) {\n    uint256 len = arr.length;\n    for (uint256 i = 0; i \u003C len;) {\n        total += arr[i];\n        unchecked { ++i; }\n    }\n}\n```\n\nGas per iteration:\n- Loop overhead: ~40 gas (no overflow checks)\n- CALLDATALOAD: 3 gas\n- Offset computation: ~10 gas\n- **Total: ~53 gas\u002Fiteration**\n\n### Yul\n```yul\nfunction sumYul(offset, length) -> total {\n    let end := add(offset, mul(length, 0x20))\n    for { } lt(offset, end) { offset := add(offset, 0x20) } {\n        total := add(total, calldataload(offset))\n    }\n}\n```\n\nGas per iteration:\n- Condition (lt): 1 + 3 + 3 + 3 + 10 = 20 gas\n- Body (add + calldataload): 3 + 3 = 6 gas\n- Post (add offset): 3 + 3 = 6 gas\n- Jump back: 11 gas\n- **Total: ~43 gas\u002Fiteration**\n\n### Benchmark Results (100 elements)\n\n| Method | Gas per iteration | Total (100 elements) | Savings vs Solidity |\n|--------|-------------------|----------------------|---------------------|\n| Solidity (checked) | ~73 | 7,300 | baseline |\n| Solidity (unchecked) | ~53 | 5,300 | 27% |\n| Yul | ~43 | 4,300 | 41% |\n\nThe savings come from three sources: no overflow checks, direct calldata offset arithmetic (instead of Solidity's bounds checking), and tighter loop structure.\n\n## Switch vs If: Choosing the Right Conditional\n\nYul provides two conditional constructs. Use the right one.\n\n### If (Single Condition)\n```yul\nif iszero(value) {\n    revert(0, 0)\n}\n```\n\nCompiles to:\n```\nDUP1            \u002F\u002F 3 gas\nISZERO          \u002F\u002F 3 gas\nPUSH2 \u003Cskip>    \u002F\u002F 3 gas\nJUMPI           \u002F\u002F 10 gas\n\u002F\u002F ... revert ...\nJUMPDEST        \u002F\u002F 1 gas\n```\n\nTotal: 20 gas for the condition check.\n\n### Switch (Multiple Conditions)\n```yul\nswitch selector\ncase 0xa9059cbb { transferImpl() }   \u002F\u002F transfer\ncase 0x70a08231 { balanceOfImpl() }  \u002F\u002F balanceOf\ncase 0x18160ddd { totalSupplyImpl() } \u002F\u002F totalSupply\ndefault { revert(0, 0) }\n```\n\nThe Yul compiler generates sequential comparisons:\n```\nDUP1 PUSH4 0xa9059cbb EQ PUSH2 \u003Ccase1> JUMPI   \u002F\u002F 22 gas\nDUP1 PUSH4 0x70a08231 EQ PUSH2 \u003Ccase2> JUMPI   \u002F\u002F 22 gas\nDUP1 PUSH4 0x18160ddd EQ PUSH2 \u003Ccase3> JUMPI   \u002F\u002F 22 gas\n\u002F\u002F default: revert\n```\n\nEach case costs 22 gas to check. For N cases, the worst case is N * 22 gas. Solidity optimizes this with a binary search tree for large function counts, but in Yul you must implement that yourself if needed.\n\n**Optimization: Order cases by frequency.** Put the most-called function selector first:\n\n```yul\n\u002F\u002F If 80% of calls are transfers, check it first\nswitch selector\ncase 0xa9059cbb { transferImpl() }    \u002F\u002F Most common: checked first\ncase 0x70a08231 { balanceOfImpl() }   \u002F\u002F Second most common\ndefault { revert(0, 0) }\n```\n\n## Unchecked Arithmetic in Yul\n\nAll arithmetic in Yul is unchecked by default. There are no overflow or underflow reverts. This is both a feature (performance) and a danger (bugs).\n\n```yul\n\u002F\u002F These all silently wrap:\nlet a := add(0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff, 1)  \u002F\u002F a = 0\nlet b := sub(0, 1)     \u002F\u002F b = 2^256 - 1\nlet c := mul(0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff, 2)  \u002F\u002F c = 2^256 - 2\n```\n\nWhen is unchecked arithmetic safe?\n\n1. **Loop counters** — If the bound is known to be \u003C 2^256 (always true in practice)\n2. **Array index calculation** — `offset + i * 32` where i \u003C array.length and array.length fits in uint256\n3. **Token amounts with bounded totals** — If totalSupply \u003C= 2^128, no addition of two balances can overflow\n4. **Subtraction after comparison** — `if gt(a, b) { let diff := sub(a, b) }` is always safe\n\nWhen you need overflow checking in Yul:\n\n```yul\nfunction safeAdd(a, b) -> c {\n    c := add(a, b)\n    if lt(c, a) { revert(0, 0) }  \u002F\u002F overflow check\n}\n\nfunction safeSub(a, b) -> c {\n    if lt(a, b) { revert(0, 0) }  \u002F\u002F underflow check\n    c := sub(a, b)\n}\n\nfunction safeMul(a, b) -> c {\n    if and(b, gt(a, div(not(0), b))) { revert(0, 0) }\n    c := mul(a, b)\n}\n```\n\n## Loop Unrolling\n\nLoop unrolling reduces the overhead per element by processing multiple elements per iteration:\n\n```yul\n\u002F\u002F Standard loop: 40 gas overhead per iteration\nfunction sum(offset, length) -> total {\n    let end := add(offset, mul(length, 0x20))\n    for { } lt(offset, end) { offset := add(offset, 0x20) } {\n        total := add(total, calldataload(offset))\n    }\n}\n\n\u002F\u002F Unrolled 4x: ~13 gas overhead per element\nfunction sumUnrolled(offset, length) -> total {\n    let end := add(offset, mul(length, 0x20))\n\n    \u002F\u002F Process 4 elements at a time\n    let endAligned := sub(end, mod(mul(length, 0x20), 0x80))\n    for { } lt(offset, endAligned) { offset := add(offset, 0x80) } {\n        total := add(total, calldataload(offset))\n        total := add(total, calldataload(add(offset, 0x20)))\n        total := add(total, calldataload(add(offset, 0x40)))\n        total := add(total, calldataload(add(offset, 0x60)))\n    }\n\n    \u002F\u002F Handle remaining elements\n    for { } lt(offset, end) { offset := add(offset, 0x20) } {\n        total := add(total, calldataload(offset))\n    }\n}\n```\n\nThe unrolled version amortizes the loop overhead (condition check + jump) across 4 elements. In practice, this saves 10-20% gas for large arrays.\n\n## Minimizing Stack Depth in Loops\n\nThe EVM stack is limited, and deep stacks mean more SWAP\u002FDUP operations. Keep loop bodies shallow:\n\n```yul\n\u002F\u002F BAD: many live variables in loop body\nfor { let i := 0 } lt(i, len) { i := add(i, 1) } {\n    let a := calldataload(add(offset, mul(i, 0x20)))\n    let b := sload(add(baseSlot, i))\n    let c := add(a, b)\n    let d := mul(c, price)\n    let e := div(d, 1000)\n    total := add(total, e)\n}\n\n\u002F\u002F BETTER: use helper function to reduce stack depth\nfunction processElement(offset, slot, price) -> result {\n    let a := calldataload(offset)\n    let b := sload(slot)\n    result := div(mul(add(a, b), price), 1000)\n}\n\nfor { let i := 0 } lt(i, len) { i := add(i, 1) } {\n    total := add(total, processElement(\n        add(offset, mul(i, 0x20)),\n        add(baseSlot, i),\n        price\n    ))\n}\n```\n\n## Early Exit Patterns\n\nWhen searching for an element, exit the loop as soon as you find it:\n\n```yul\n\u002F\u002F Find the index of 'target' in a calldata array\nfunction indexOf(offset, length, target) -> index, found {\n    let end := add(offset, mul(length, 0x20))\n    for { index := 0 } lt(offset, end) { offset := add(offset, 0x20) } {\n        if eq(calldataload(offset), target) {\n            found := 1\n            \u002F\u002F Yul has no 'break' — use a function return instead\n            leave  \u002F\u002F exits the function immediately\n        }\n        index := add(index, 1)\n    }\n}\n```\n\nThe `leave` statement is Yul's equivalent of `return` — it exits the current function immediately. Use it for early exits in search loops.\n\n## Bit Manipulation as Loop Alternative\n\nSometimes you can replace a loop with bitwise operations:\n\n```yul\n\u002F\u002F Count the number of set bits (population count)\n\u002F\u002F Loop approach: up to 256 iterations\nfunction popcountLoop(x) -> count {\n    for { } x { x := shr(1, x) } {\n        count := add(count, and(x, 1))\n    }\n}\n\n\u002F\u002F Bitwise approach: always 5 steps (Brian Kernighan's algorithm)\nfunction popcountFast(x) -> count {\n    for { } x { } {\n        x := and(x, sub(x, 1))  \u002F\u002F clear lowest set bit\n        count := add(count, 1)\n    }\n}\n\n\u002F\u002F Even faster: parallel bit counting (no loop)\nfunction popcountParallel(x) -> count {\n    x := sub(x, and(shr(1, x), 0x5555555555555555555555555555555555555555555555555555555555555555))\n    x := add(and(x, 0x3333333333333333333333333333333333333333333333333333333333333333),\n             and(shr(2, x), 0x3333333333333333333333333333333333333333333333333333333333333333))\n    x := and(add(x, shr(4, x)), 0x0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f)\n    count := mod(mul(x, 0x0101010101010101010101010101010101010101010101010101010101010101), exp(2, 248))\n}\n```\n\n## Practical Example: Gas-Efficient Array Comparison\n\nCompare two calldata arrays for equality:\n\n```yul\nfunction arraysEqual(offset1, offset2, length) -> equal {\n    equal := 1\n    let end := add(offset1, mul(length, 0x20))\n    for { } lt(offset1, end) { } {\n        if iszero(eq(calldataload(offset1), calldataload(offset2))) {\n            equal := 0\n            leave\n        }\n        offset1 := add(offset1, 0x20)\n        offset2 := add(offset2, 0x20)\n    }\n}\n```\n\nThis exits on the first mismatch. For MEV applications, this pattern is used to verify expected pool states before executing a trade.\n\n## Gas Cost Summary: Loop Patterns\n\n| Pattern | Gas per iteration (approximate) |\n|---------|--------------------------------|\n| Solidity for (checked) | 73 |\n| Solidity for (unchecked) | 53 |\n| Yul for | 43 |\n| Yul for (unrolled 4x) | 35 |\n| Yul while (sentinel) | 40 |\n\nThe absolute numbers depend on the loop body, but the relative savings are consistent: Yul loops save 30-50% over standard Solidity.\n\n## Conclusion\n\nLoop optimization in Yul is not about clever tricks — it is about understanding the exact gas cost of every opcode in your loop body and systematically eliminating waste. Cache storage reads, unroll tight loops, exit early, and prefer bitwise operations over iteration where possible. These patterns compound: a 30% loop savings in a function called 1000 times per block is 30% more profit for your MEV bot.\n\nIn the final article, we put everything together and build a complete token swap contract in pure Yul.","\u003Ch2 id=\"why-loops-are-where-gas-goes-to-die\">Why Loops Are Where Gas Goes to Die\u003C\u002Fh2>\n\u003Cp>In most smart contracts, the single largest gas consumer is iteration. A function that loops over an array of 100 elements executes its body 100 times — and every opcode inside that body is multiplied by 100. A single unnecessary SLOAD inside a loop costs 10,000-210,000 gas. An unoptimized loop counter adds 4,000 gas over 100 iterations.\u003C\u002Fp>\n\u003Cp>This article breaks down the exact gas cost of loop constructs in Yul and Solidity, and shows patterns that cut loop gas by 30-60%.\u003C\u002Fp>\n\u003Ch2 id=\"anatomy-of-a-yul-for-loop\">Anatomy of a Yul For-Loop\u003C\u002Fh2>\n\u003Cpre>\u003Ccode class=\"language-yul\">for { let i := 0 } lt(i, 10) { i := add(i, 1) } {\n    \u002F\u002F body\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>This compiles to the following opcode sequence:\u003C\u002Fp>\n\u003Cpre>\u003Ccode>\u002F\u002F Initialization: let i := 0\nPUSH1 0x00          \u002F\u002F 3 gas (executed once)\n\n\u002F\u002F Condition check: lt(i, 10)\nJUMPDEST            \u002F\u002F 1 gas (loop entry point)\nDUP1                \u002F\u002F 3 gas\nPUSH1 0x0a          \u002F\u002F 3 gas\nLT                  \u002F\u002F 3 gas\nPUSH2 &lt;exit&gt;        \u002F\u002F 3 gas\nJUMPI               \u002F\u002F 10 gas (conditional jump)\n\n\u002F\u002F Body: (varies)\n...\n\n\u002F\u002F Post-iteration: i := add(i, 1)\nPUSH1 0x01          \u002F\u002F 3 gas\nADD                 \u002F\u002F 3 gas\n\n\u002F\u002F Jump back to condition\nPUSH2 &lt;loop_start&gt;  \u002F\u002F 3 gas\nJUMP                \u002F\u002F 8 gas\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Per iteration, the loop overhead is:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>Condition check: 1 + 3 + 3 + 3 + 3 + 10 = \u003Cstrong>23 gas\u003C\u002Fstrong>\u003C\u002Fli>\n\u003Cli>Post-iteration: 3 + 3 = \u003Cstrong>6 gas\u003C\u002Fstrong>\u003C\u002Fli>\n\u003Cli>Jump back: 3 + 8 = \u003Cstrong>11 gas\u003C\u002Fstrong>\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Total overhead per iteration: 40 gas\u003C\u002Fstrong>\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>For 100 iterations, that is 4,000 gas just in loop mechanics — before any work in the body.\u003C\u002Fp>\n\u003Ch2 id=\"solidity-loop-vs-yul-loop-a-benchmark\">Solidity Loop vs Yul Loop: A Benchmark\u003C\u002Fh2>\n\u003Cp>Let us compare summing a \u003Ccode>uint256[] calldata\u003C\u002Fcode> array:\u003C\u002Fp>\n\u003Ch3>Solidity (Checked Arithmetic)\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-solidity\">function sumSolidity(uint256[] calldata arr) external pure returns (uint256 total) {\n    for (uint256 i = 0; i &lt; arr.length; i++) {\n        total += arr[i];\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Gas per iteration:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>Loop overhead: ~60 gas (Solidity adds overflow checks on \u003Ccode>i++\u003C\u002Fcode> and \u003Ccode>total +=\u003C\u002Fcode>)\u003C\u002Fli>\n\u003Cli>CALLDATALOAD: 3 gas\u003C\u002Fli>\n\u003Cli>Offset computation: ~10 gas\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Total: ~73 gas\u002Fiteration\u003C\u002Fstrong>\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch3>Solidity (Unchecked)\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-solidity\">function sumUnchecked(uint256[] calldata arr) external pure returns (uint256 total) {\n    uint256 len = arr.length;\n    for (uint256 i = 0; i &lt; len;) {\n        total += arr[i];\n        unchecked { ++i; }\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Gas per iteration:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>Loop overhead: ~40 gas (no overflow checks)\u003C\u002Fli>\n\u003Cli>CALLDATALOAD: 3 gas\u003C\u002Fli>\n\u003Cli>Offset computation: ~10 gas\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Total: ~53 gas\u002Fiteration\u003C\u002Fstrong>\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch3>Yul\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-yul\">function sumYul(offset, length) -&gt; total {\n    let end := add(offset, mul(length, 0x20))\n    for { } lt(offset, end) { offset := add(offset, 0x20) } {\n        total := add(total, calldataload(offset))\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Gas per iteration:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>Condition (lt): 1 + 3 + 3 + 3 + 10 = 20 gas\u003C\u002Fli>\n\u003Cli>Body (add + calldataload): 3 + 3 = 6 gas\u003C\u002Fli>\n\u003Cli>Post (add offset): 3 + 3 = 6 gas\u003C\u002Fli>\n\u003Cli>Jump back: 11 gas\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Total: ~43 gas\u002Fiteration\u003C\u002Fstrong>\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch3>Benchmark Results (100 elements)\u003C\u002Fh3>\n\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>Method\u003C\u002Fth>\u003Cth>Gas per iteration\u003C\u002Fth>\u003Cth>Total (100 elements)\u003C\u002Fth>\u003Cth>Savings vs Solidity\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\n\u003Ctr>\u003Ctd>Solidity (checked)\u003C\u002Ftd>\u003Ctd>~73\u003C\u002Ftd>\u003Ctd>7,300\u003C\u002Ftd>\u003Ctd>baseline\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>Solidity (unchecked)\u003C\u002Ftd>\u003Ctd>~53\u003C\u002Ftd>\u003Ctd>5,300\u003C\u002Ftd>\u003Ctd>27%\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>Yul\u003C\u002Ftd>\u003Ctd>~43\u003C\u002Ftd>\u003Ctd>4,300\u003C\u002Ftd>\u003Ctd>41%\u003C\u002Ftd>\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\n\u003Cp>The savings come from three sources: no overflow checks, direct calldata offset arithmetic (instead of Solidity’s bounds checking), and tighter loop structure.\u003C\u002Fp>\n\u003Ch2 id=\"switch-vs-if-choosing-the-right-conditional\">Switch vs If: Choosing the Right Conditional\u003C\u002Fh2>\n\u003Cp>Yul provides two conditional constructs. Use the right one.\u003C\u002Fp>\n\u003Ch3>If (Single Condition)\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-yul\">if iszero(value) {\n    revert(0, 0)\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Compiles to:\u003C\u002Fp>\n\u003Cpre>\u003Ccode>DUP1            \u002F\u002F 3 gas\nISZERO          \u002F\u002F 3 gas\nPUSH2 &lt;skip&gt;    \u002F\u002F 3 gas\nJUMPI           \u002F\u002F 10 gas\n\u002F\u002F ... revert ...\nJUMPDEST        \u002F\u002F 1 gas\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Total: 20 gas for the condition check.\u003C\u002Fp>\n\u003Ch3>Switch (Multiple Conditions)\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-yul\">switch selector\ncase 0xa9059cbb { transferImpl() }   \u002F\u002F transfer\ncase 0x70a08231 { balanceOfImpl() }  \u002F\u002F balanceOf\ncase 0x18160ddd { totalSupplyImpl() } \u002F\u002F totalSupply\ndefault { revert(0, 0) }\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>The Yul compiler generates sequential comparisons:\u003C\u002Fp>\n\u003Cpre>\u003Ccode>DUP1 PUSH4 0xa9059cbb EQ PUSH2 &lt;case1&gt; JUMPI   \u002F\u002F 22 gas\nDUP1 PUSH4 0x70a08231 EQ PUSH2 &lt;case2&gt; JUMPI   \u002F\u002F 22 gas\nDUP1 PUSH4 0x18160ddd EQ PUSH2 &lt;case3&gt; JUMPI   \u002F\u002F 22 gas\n\u002F\u002F default: revert\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Each case costs 22 gas to check. For N cases, the worst case is N * 22 gas. Solidity optimizes this with a binary search tree for large function counts, but in Yul you must implement that yourself if needed.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>Optimization: Order cases by frequency.\u003C\u002Fstrong> Put the most-called function selector first:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-yul\">\u002F\u002F If 80% of calls are transfers, check it first\nswitch selector\ncase 0xa9059cbb { transferImpl() }    \u002F\u002F Most common: checked first\ncase 0x70a08231 { balanceOfImpl() }   \u002F\u002F Second most common\ndefault { revert(0, 0) }\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"unchecked-arithmetic-in-yul\">Unchecked Arithmetic in Yul\u003C\u002Fh2>\n\u003Cp>All arithmetic in Yul is unchecked by default. There are no overflow or underflow reverts. This is both a feature (performance) and a danger (bugs).\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-yul\">\u002F\u002F These all silently wrap:\nlet a := add(0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff, 1)  \u002F\u002F a = 0\nlet b := sub(0, 1)     \u002F\u002F b = 2^256 - 1\nlet c := mul(0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff, 2)  \u002F\u002F c = 2^256 - 2\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>When is unchecked arithmetic safe?\u003C\u002Fp>\n\u003Col>\n\u003Cli>\u003Cstrong>Loop counters\u003C\u002Fstrong> — If the bound is known to be &lt; 2^256 (always true in practice)\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Array index calculation\u003C\u002Fstrong> — \u003Ccode>offset + i * 32\u003C\u002Fcode> where i &lt; array.length and array.length fits in uint256\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Token amounts with bounded totals\u003C\u002Fstrong> — If totalSupply &lt;= 2^128, no addition of two balances can overflow\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Subtraction after comparison\u003C\u002Fstrong> — \u003Ccode>if gt(a, b) { let diff := sub(a, b) }\u003C\u002Fcode> is always safe\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>When you need overflow checking in Yul:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-yul\">function safeAdd(a, b) -&gt; c {\n    c := add(a, b)\n    if lt(c, a) { revert(0, 0) }  \u002F\u002F overflow check\n}\n\nfunction safeSub(a, b) -&gt; c {\n    if lt(a, b) { revert(0, 0) }  \u002F\u002F underflow check\n    c := sub(a, b)\n}\n\nfunction safeMul(a, b) -&gt; c {\n    if and(b, gt(a, div(not(0), b))) { revert(0, 0) }\n    c := mul(a, b)\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"loop-unrolling\">Loop Unrolling\u003C\u002Fh2>\n\u003Cp>Loop unrolling reduces the overhead per element by processing multiple elements per iteration:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-yul\">\u002F\u002F Standard loop: 40 gas overhead per iteration\nfunction sum(offset, length) -&gt; total {\n    let end := add(offset, mul(length, 0x20))\n    for { } lt(offset, end) { offset := add(offset, 0x20) } {\n        total := add(total, calldataload(offset))\n    }\n}\n\n\u002F\u002F Unrolled 4x: ~13 gas overhead per element\nfunction sumUnrolled(offset, length) -&gt; total {\n    let end := add(offset, mul(length, 0x20))\n\n    \u002F\u002F Process 4 elements at a time\n    let endAligned := sub(end, mod(mul(length, 0x20), 0x80))\n    for { } lt(offset, endAligned) { offset := add(offset, 0x80) } {\n        total := add(total, calldataload(offset))\n        total := add(total, calldataload(add(offset, 0x20)))\n        total := add(total, calldataload(add(offset, 0x40)))\n        total := add(total, calldataload(add(offset, 0x60)))\n    }\n\n    \u002F\u002F Handle remaining elements\n    for { } lt(offset, end) { offset := add(offset, 0x20) } {\n        total := add(total, calldataload(offset))\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>The unrolled version amortizes the loop overhead (condition check + jump) across 4 elements. In practice, this saves 10-20% gas for large arrays.\u003C\u002Fp>\n\u003Ch2 id=\"minimizing-stack-depth-in-loops\">Minimizing Stack Depth in Loops\u003C\u002Fh2>\n\u003Cp>The EVM stack is limited, and deep stacks mean more SWAP\u002FDUP operations. Keep loop bodies shallow:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-yul\">\u002F\u002F BAD: many live variables in loop body\nfor { let i := 0 } lt(i, len) { i := add(i, 1) } {\n    let a := calldataload(add(offset, mul(i, 0x20)))\n    let b := sload(add(baseSlot, i))\n    let c := add(a, b)\n    let d := mul(c, price)\n    let e := div(d, 1000)\n    total := add(total, e)\n}\n\n\u002F\u002F BETTER: use helper function to reduce stack depth\nfunction processElement(offset, slot, price) -&gt; result {\n    let a := calldataload(offset)\n    let b := sload(slot)\n    result := div(mul(add(a, b), price), 1000)\n}\n\nfor { let i := 0 } lt(i, len) { i := add(i, 1) } {\n    total := add(total, processElement(\n        add(offset, mul(i, 0x20)),\n        add(baseSlot, i),\n        price\n    ))\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"early-exit-patterns\">Early Exit Patterns\u003C\u002Fh2>\n\u003Cp>When searching for an element, exit the loop as soon as you find it:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-yul\">\u002F\u002F Find the index of 'target' in a calldata array\nfunction indexOf(offset, length, target) -&gt; index, found {\n    let end := add(offset, mul(length, 0x20))\n    for { index := 0 } lt(offset, end) { offset := add(offset, 0x20) } {\n        if eq(calldataload(offset), target) {\n            found := 1\n            \u002F\u002F Yul has no 'break' — use a function return instead\n            leave  \u002F\u002F exits the function immediately\n        }\n        index := add(index, 1)\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>The \u003Ccode>leave\u003C\u002Fcode> statement is Yul’s equivalent of \u003Ccode>return\u003C\u002Fcode> — it exits the current function immediately. Use it for early exits in search loops.\u003C\u002Fp>\n\u003Ch2 id=\"bit-manipulation-as-loop-alternative\">Bit Manipulation as Loop Alternative\u003C\u002Fh2>\n\u003Cp>Sometimes you can replace a loop with bitwise operations:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-yul\">\u002F\u002F Count the number of set bits (population count)\n\u002F\u002F Loop approach: up to 256 iterations\nfunction popcountLoop(x) -&gt; count {\n    for { } x { x := shr(1, x) } {\n        count := add(count, and(x, 1))\n    }\n}\n\n\u002F\u002F Bitwise approach: always 5 steps (Brian Kernighan's algorithm)\nfunction popcountFast(x) -&gt; count {\n    for { } x { } {\n        x := and(x, sub(x, 1))  \u002F\u002F clear lowest set bit\n        count := add(count, 1)\n    }\n}\n\n\u002F\u002F Even faster: parallel bit counting (no loop)\nfunction popcountParallel(x) -&gt; count {\n    x := sub(x, and(shr(1, x), 0x5555555555555555555555555555555555555555555555555555555555555555))\n    x := add(and(x, 0x3333333333333333333333333333333333333333333333333333333333333333),\n             and(shr(2, x), 0x3333333333333333333333333333333333333333333333333333333333333333))\n    x := and(add(x, shr(4, x)), 0x0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f)\n    count := mod(mul(x, 0x0101010101010101010101010101010101010101010101010101010101010101), exp(2, 248))\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"practical-example-gas-efficient-array-comparison\">Practical Example: Gas-Efficient Array Comparison\u003C\u002Fh2>\n\u003Cp>Compare two calldata arrays for equality:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-yul\">function arraysEqual(offset1, offset2, length) -&gt; equal {\n    equal := 1\n    let end := add(offset1, mul(length, 0x20))\n    for { } lt(offset1, end) { } {\n        if iszero(eq(calldataload(offset1), calldataload(offset2))) {\n            equal := 0\n            leave\n        }\n        offset1 := add(offset1, 0x20)\n        offset2 := add(offset2, 0x20)\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>This exits on the first mismatch. For MEV applications, this pattern is used to verify expected pool states before executing a trade.\u003C\u002Fp>\n\u003Ch2 id=\"gas-cost-summary-loop-patterns\">Gas Cost Summary: Loop Patterns\u003C\u002Fh2>\n\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>Pattern\u003C\u002Fth>\u003Cth>Gas per iteration (approximate)\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\n\u003Ctr>\u003Ctd>Solidity for (checked)\u003C\u002Ftd>\u003Ctd>73\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>Solidity for (unchecked)\u003C\u002Ftd>\u003Ctd>53\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>Yul for\u003C\u002Ftd>\u003Ctd>43\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>Yul for (unrolled 4x)\u003C\u002Ftd>\u003Ctd>35\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>Yul while (sentinel)\u003C\u002Ftd>\u003Ctd>40\u003C\u002Ftd>\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\n\u003Cp>The absolute numbers depend on the loop body, but the relative savings are consistent: Yul loops save 30-50% over standard Solidity.\u003C\u002Fp>\n\u003Ch2 id=\"conclusion\">Conclusion\u003C\u002Fh2>\n\u003Cp>Loop optimization in Yul is not about clever tricks — it is about understanding the exact gas cost of every opcode in your loop body and systematically eliminating waste. Cache storage reads, unroll tight loops, exit early, and prefer bitwise operations over iteration where possible. These patterns compound: a 30% loop savings in a function called 1000 times per block is 30% more profit for your MEV bot.\u003C\u002Fp>\n\u003Cp>In the final article, we put everything together and build a complete token swap contract in pure Yul.\u003C\u002Fp>\n","en","b0000000-0000-0000-0000-000000000001",true,"2026-03-28T10:44:22.694790Z","Optimize EVM loops in Yul: gas anatomy of for-loops, switch vs if, unchecked arithmetic, loop unrolling, and benchmarks comparing Solidity and Yul iteration.","Yul gas efficient loops",null,"index, follow",[21,26,30,34],{"id":22,"name":23,"slug":24,"created_at":25},"c0000000-0000-0000-0000-000000000016","EVM","evm","2026-03-28T10:44:21.513630Z",{"id":27,"name":28,"slug":29,"created_at":25},"c0000000-0000-0000-0000-000000000020","Gas Optimization","gas-optimization",{"id":31,"name":32,"slug":33,"created_at":25},"c0000000-0000-0000-0000-000000000014","Solidity","solidity",{"id":35,"name":36,"slug":37,"created_at":25},"c0000000-0000-0000-0000-000000000018","Yul","yul","Blockchain",[40,46,52],{"id":41,"title":42,"slug":43,"excerpt":44,"locale":12,"category_name":38,"published_at":45},"de000000-0000-0000-0000-000000000003","The Ethereum Interoperability Layer: How 55+ L2s Become One Chain","ethereum-interoperability-layer-how-55-l2s-become-one-chain","Ethereum has 55+ Layer 2 rollups, fragmenting liquidity and user experience. The Ethereum Interoperability Layer — combining cross-rollup messaging, shared sequencers, and based rollups — aims to unify them into a single composable network.","2026-03-28T10:44:35.632478Z",{"id":47,"title":48,"slug":49,"excerpt":50,"locale":12,"category_name":38,"published_at":51},"de000000-0000-0000-0000-000000000002","ZK Proofs Beyond Rollups: Verifiable AI Inference on Ethereum","zk-proofs-beyond-rollups-verifiable-ai-inference-ethereum","Zero-knowledge proofs are no longer just a scaling tool. In 2026, zkML enables verifiable AI inference on-chain, ZK coprocessors move heavy computation off-chain with on-chain verification, and new proving systems like SP1 and Jolt make it practical.","2026-03-28T10:44:35.618408Z",{"id":53,"title":54,"slug":55,"excerpt":56,"locale":12,"category_name":38,"published_at":57},"dd000000-0000-0000-0000-000000000003","EIP-7702 in Practice: Building Smart Account Flows After Pectra","eip-7702-in-practice-building-smart-account-flows-after-pectra","EIP-7702 lets any Ethereum EOA temporarily act as a smart contract within a single transaction. Here is how to implement batch transactions, gas sponsorship, and social recovery using the new account abstraction primitive.","2026-03-28T10:44:35.031290Z",{"id":13,"name":59,"slug":60,"bio":61,"photo_url":18,"linkedin":18,"role":62,"created_at":63,"updated_at":63},"Open Soft Team","open-soft-team","The engineering team at Open Soft, building premium software solutions from Bali, Indonesia.","Engineering Team","2026-03-28T08:31:22.226811Z"]