Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace runtime checks with compile-time checks for div and mod operations on constant denominator #13206

Open
ZumZoom opened this issue Jun 24, 2022 · 2 comments

Comments

@ZumZoom
Copy link

@ZumZoom ZumZoom commented Jun 24, 2022

When doing division and modulo operation with constant or immutable denominator it is possible to only do compile-time denominator check and skip runtime checks.

Consider following contracts:

pragma solidity 0.8.15;

uint256 constant _MODULE = 17;

contract C1 {
    function f(uint256 a) external pure returns(uint256) {
        return a % _MODULE;
    }
}

contract C2 {
    function f(uint256 a) external pure returns(uint256) {
        unchecked {
            return a % _MODULE;
        }
    }
}

contract C3 {
    function f(uint256 a) external pure returns(uint256) {
        return a / _MODULE;
    }
}

contract C4 {
    function f(uint256 a) external pure returns(uint256) {
        unchecked {
            return a / _MODULE;
        }
    }
}

Even though at compile-time it is known that there is no case when division by zero will happen, generated code for checked case is more complicated than for unchecked case.

Corresponding gas costs of each function:

C1.f: 317
C2.f: 246
C3.f: 317
C4.f: 246
@ZumZoom ZumZoom changed the title Replace runtime checks with compile-time checks for div and mod operations on constants Replace runtime checks with compile-time checks for div and mod operations on constant denominator Jun 24, 2022
@cameel cameel added this to New issues in Solidity via automation Jul 4, 2022
@timweri
Copy link
Contributor

@timweri timweri commented Jul 4, 2022

Hi. I'd like to attempt implementing this. I'm still in the process of tracing how the optimizer is implemented.

@cameel
Copy link
Member

@cameel cameel commented Jul 5, 2022

@timweri I wanted to give you some hints but after looking into it I'm not sure if there's actually anything to do here. And if there is, I'd rate this as medium difficulty because jumping into the optimizer is definitely much more effort than the trivial tasks we usually rate as easy.

@ZumZoom Have you tried the new IR-based pipeline (i.e. the --via-ir option)? It seems that the Yul optimizer can deal with this case just fine. See my example below. There's probably not much point trying to do it for the legacy codegen and it's likely much harder there (that's why Yul optimizer was introduced in the first place).

Example

Let's take parts of your example and compare the compiler output using both pipelines.

checked.sol:

uint256 constant _MODULE = 17;

contract C {
    function f(uint256 a) external pure returns(uint256) {
        return a / _MODULE;
    }
}

unchecked.sol:

uint256 constant _MODULE = 17;

contract C {
    function f(uint256 a) external pure returns(uint256) {
        unchecked {
            return a / _MODULE;
        }
    }
}

IR pipeline

diff --unified \
    <(solc checked.sol --optimize --ir-optimized --debug-info none) \
    <(solc unchecked.sol --optimize --ir-optimized --debug-info none)

Here you get code that's pretty much identical. Only slight differences in naming that should not affect bytecode (and cost):

@@ -1,18 +1,18 @@
 Optimized IR:
-/// @use-src 0:"checked.sol"
-object "C_16" {
+/// @use-src 0:"unchecked.sol"
+object "C_17" {
     code {
         {
             let _1 := memoryguard(0x80)
             mstore(64, _1)
             if callvalue() { revert(0, 0) }
-            let _2 := datasize("C_16_deployed")
-            codecopy(_1, dataoffset("C_16_deployed"), _2)
+            let _2 := datasize("C_17_deployed")
+            codecopy(_1, dataoffset("C_17_deployed"), _2)
             return(_1, _2)
         }
     }
-    /// @use-src 0:"checked.sol"
-    object "C_16_deployed" {
+    /// @use-src 0:"unchecked.sol"
+    object "C_17_deployed" {
         code {
             {
                 let _1 := memoryguard(0x80)
@@ -30,7 +30,7 @@
                 revert(0, 0)
             }
         }

Legacy pipeline

diff --unified \
    <(solc checked.sol --optimize --asm --debug-info none) \
    <(solc unchecked.sol --optimize --asm --debug-info none)

Via the legacy pipeline the unchecked version is clearly shorter:

-======= checked.sol:C =======
+======= unchecked.sol:C =======
 EVM assembly:
   mstore(0x40, 0x80)
   callvalue
@@ -52,8 +52,11 @@
       tag_6
       jump     // in
     tag_5:
-      tag_7
-      jump     // in
+      0x11
+      swap1
+      div
+      swap1
+      jump
     tag_4:
       mload(0x40)
       swap1
@@ -67,19 +70,6 @@
       sub
       swap1
       return
-    tag_7:
-      0x00
-      tag_11
-      0x11
-      dup4
-      tag_12
-      jump     // in
-    tag_11:
-      swap3
-      swap2
-      pop
-      pop
-      jump     // out
     tag_6:
       0x00
       0x20
@@ -88,40 +78,19 @@
       sub
       slt
       iszero
-      tag_15
+      tag_16
       jumpi
       0x00
       dup1
       revert
-    tag_15:
+    tag_16:
       pop
       calldataload
       swap2
       swap1
       pop
       jump     // out
-    tag_12:
-      0x00
-      dup3
-      tag_18
-      jumpi
-      0x4e487b71
-      0xe0
-      shl
-      0x00
-      mstore
-      0x12
-      0x04
-      mstore
-      0x24
-      0x00
-      revert
-    tag_18:
-      pop
-      div
-      swap1
-      jump     // out

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Solidity
  
New issues
Development

No branches or pull requests

4 participants