Unsigned Arithmetic Gas Cost

Under solc 0.8.19 (EVM version “Paris”), using non-constant operands, my measurement shows:

+-----------+---------+-----------+------------+
| Operation | Checked | Unchecked | Difference |
+-----------+---------+-----------+------------+
|     +     |    87   |     21    |     66     |
+-----------+---------+-----------+------------+
|     -     |    87   |     21    |     66     |
+-----------+---------+-----------+------------+
|     *     |   103   |     23    |     80     |
+-----------+---------+-----------+------------+
|     /     |    72   |     40    |     32     |
+-----------+---------+-----------+------------+
|     &     |    21   |     21    |      0     |
+-----------+---------+-----------+------------+
|     |     |    21   |     21    |      0     |
+-----------+---------+-----------+------------+
|     ^     |    21   |     21    |      0     |
+-----------+---------+-----------+------------+
|     <<    |    30   |     30    |      0     |
+-----------+---------+-----------+------------+
|     >>    |    30   |     30    |      0     |
+-----------+---------+-----------+------------+

As you can see, unchecked division is cheaper than checked division despite the fact that no overflow or underflow can ever take place.

The difference may lie in the fact that upon division by zero, checked division refunds the caller with the remaining gas (revert), while unchecked division doesn’t (assert).

But I’m pretty sure that on recent EVM versions, both revert and assert refund the caller with the remaining gas.

Does anybody happen to know the benefit in using checked division over unchecked division?

Can you elaborate on how you took the measurements here? On a simple example I get identical code when compiling a simple function performing an unsigned division whether unchecked or not.

Ah, ok, I checked with via-IR code generation. I guess you’re using legacy code generation? All checked arithmetic is implemented as Yul functions, which generally induces some overhead when used from legacy code generation, and this includes unsigned division. With via-IR code generation you won’t see a difference. But to answer the question directly: there is no benefit of using checked division over unchecked division for unsigned types.

Thank you!

FWIW:

Onchain:

// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.19;

contract Test {
    uint256 public gasUsed;

    function cAdd(uint256 x, uint256 y) external returns (uint256 z) {          uint256 gasLeft = gasleft(); z = x + y; gasUsed = gasLeft - gasleft(); }
    function cSub(uint256 x, uint256 y) external returns (uint256 z) {          uint256 gasLeft = gasleft(); z = x - y; gasUsed = gasLeft - gasleft(); }
    function cMul(uint256 x, uint256 y) external returns (uint256 z) {          uint256 gasLeft = gasleft(); z = x * y; gasUsed = gasLeft - gasleft(); }
    function cDiv(uint256 x, uint256 y) external returns (uint256 z) {          uint256 gasLeft = gasleft(); z = x / y; gasUsed = gasLeft - gasleft(); }
    function uAdd(uint256 x, uint256 y) external returns (uint256 z) {unchecked{uint256 gasLeft = gasleft(); z = x + y; gasUsed = gasLeft - gasleft();}}
    function uSub(uint256 x, uint256 y) external returns (uint256 z) {unchecked{uint256 gasLeft = gasleft(); z = x - y; gasUsed = gasLeft - gasleft();}}
    function uMul(uint256 x, uint256 y) external returns (uint256 z) {unchecked{uint256 gasLeft = gasleft(); z = x * y; gasUsed = gasLeft - gasleft();}}
    function uDiv(uint256 x, uint256 y) external returns (uint256 z) {unchecked{uint256 gasLeft = gasleft(); z = x / y; gasUsed = gasLeft - gasleft();}}

    function cAnd(uint256 x, uint256 y) external returns (uint256 z) {          uint256 gasLeft = gasleft(); z = x &  y; gasUsed = gasLeft - gasleft(); }
    function cOr (uint256 x, uint256 y) external returns (uint256 z) {          uint256 gasLeft = gasleft(); z = x |  y; gasUsed = gasLeft - gasleft(); }
    function cXor(uint256 x, uint256 y) external returns (uint256 z) {          uint256 gasLeft = gasleft(); z = x ^  y; gasUsed = gasLeft - gasleft(); }
    function cShl(uint256 x, uint8   y) external returns (uint256 z) {          uint256 gasLeft = gasleft(); z = x << y; gasUsed = gasLeft - gasleft(); }
    function cShr(uint256 x, uint8   y) external returns (uint256 z) {          uint256 gasLeft = gasleft(); z = x >> y; gasUsed = gasLeft - gasleft(); }
    function uAnd(uint256 x, uint256 y) external returns (uint256 z) {unchecked{uint256 gasLeft = gasleft(); z = x &  y; gasUsed = gasLeft - gasleft();}}
    function uOr (uint256 x, uint256 y) external returns (uint256 z) {unchecked{uint256 gasLeft = gasleft(); z = x |  y; gasUsed = gasLeft - gasleft();}}
    function uXor(uint256 x, uint256 y) external returns (uint256 z) {unchecked{uint256 gasLeft = gasleft(); z = x ^  y; gasUsed = gasLeft - gasleft();}}
    function uShl(uint256 x, uint8   y) external returns (uint256 z) {unchecked{uint256 gasLeft = gasleft(); z = x << y; gasUsed = gasLeft - gasleft();}}
    function uShr(uint256 x, uint8   y) external returns (uint256 z) {unchecked{uint256 gasLeft = gasleft(); z = x >> y; gasUsed = gasLeft - gasleft();}}
}

Offchain:

describe('Test', () => {
    for (const test of [
        {func: 'cAdd', x: 255, y: 255},
        {func: 'cSub', x: 255, y: 255},
        {func: 'cMul', x: 128, y: 128},
        {func: 'cDiv', x: 255, y: 255},
        {func: 'uAdd', x: 255, y: 255},
        {func: 'uSub', x: 255, y: 255},
        {func: 'uMul', x: 128, y: 128},
        {func: 'uDiv', x: 255, y: 255},
        {func: 'cAnd', x: 255, y: 255},
        {func: 'cOr' , x: 255, y: 255},
        {func: 'cXor', x: 255, y: 255},
        {func: 'cShl', x: 255, y: 7},
        {func: 'cShr', x: 255, y: 7},
        {func: 'uAnd', x: 255, y: 255},
        {func: 'uOr' , x: 255, y: 255},
        {func: 'uXor', x: 255, y: 255},
        {func: 'uShl', x: 255, y: 7},
        {func: 'uShr', x: 255, y: 7},
    ]) {
        it(`${test.func} gas cost`, async () => {
            const contract = await Test.deploy();
            const receipt = await contract[test.func](test.x, test.y);
            const gasUsed = await contract.gasUsed();
            console.log(gasUsed.toString());
        });
    }
});

Here is the extended table for [checked, unchecked] x [viaIR off, viaIR on]:
image
Strangely enough, it seems that unchecked division is cheaper with viaIR disabled (40) vs with viaIR enabled (56). Would you happen to know the reason for that?

Why is there no difference between unchecked and checked operations ? Unchecked are obviously prone to buffer overflows.

  1. Where do you see ‘no difference’?
  2. The table above depicts gas differences, not functional differences

Hi, I was referring to @ekpyron response where he mentioned “there is no benefit of using checked division over unchecked division for unsigned types”. There obviously is from security perspective.

  1. The context here is gas, not anything else
  2. There is no security difference in the case of division:
    • An overflow can never occur, both when checked and when unchecked
    • Revert occurs on division by zero, both when checked and when unchecked

Cool. Thanks for clarification.

1 Like