Skip to content

Commit

Permalink
add eager boolean evaluation utility functions
Browse files Browse the repository at this point in the history
  • Loading branch information
nonergodic committed Dec 18, 2024
1 parent 65f905f commit 21ea424
Show file tree
Hide file tree
Showing 8 changed files with 534 additions and 6 deletions.
45 changes: 41 additions & 4 deletions docs/Optimization.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
# Compiler Optimization

List of ways to avoid short-comings of the current optimizer which lead to suboptimal byte code
List of ways to avoid short-comings of the current optimizer which lead to suboptimal byte code.

## for loop array length checking

```
```solidity
function iterate(uint[] memory myArray) {
uint len = myArray.length;
for (uint i; i < len; ++i) { /*...*/}
}
```
is more efficient than
```
```solidity
function iterate(uint[] memory myArray) {
for (uint i; i < myArray.length; ++i) { /*...*/}
}
Expand All @@ -22,4 +22,41 @@ If `myArray` uses `calldata` instead of `memory`, both versions produce the same

## prefer `< MAX + 1` over `<= MAX` for const comparison

Given that the EVM only supports `LT` and `GT` but not `LTE` or `GTE`, solc implements `x<=y` as `!(x>y)`. However, given a constant `MAX`, since solc resolves `MAX + 1` at compile time, `< MAX + 1` saves one `ISZERO` opcode.
Given that the EVM only supports `LT` and `GT` but not `LTE` or `GTE`, solc implements `x<=y` as `!(x>y)`. However, given a constant `MAX`, since solc resolves `MAX + 1` at compile time, `< MAX + 1` saves one `ISZERO` opcode.

## consider using `eagerAnd` and `eagerOr` over short-curcuiting `&&` and `||`

Short-circuiting `lhs && rhs` requires _at least_ the insertion of:

| OpCode/ByteCode | Size | Gas | Explanation |
| --------------- | :--: | :-: | ----------------------------------------------------------- |
| `DUP1` | 1 | 3 | copy result of `lhs` which currently is on top of the stack |
| `PUSH2` | 1 | 3 | push location for code to eval/load `rhs` |
| jump offset | 2 | 0 | points to **second** `JUMPDST` |
| `JUMPI` | 1 | 10 | if `lhs` is `true` eval `rhs` too, otherwise short-circuit |
| `JUMPDST` | 1 | 1 | proceed here with the result on top of the stack |
| --------------- | ---- | --- | ----------------------------------------------------------- |
| `JUMPDST` | 1 | 1 | code to eval/load `rhs` starts here |
| `POP` | 1 | 3 | remove duplicated `true` from stack |
| --------------- | ---- | --- | ----------------------------------------------------------- |
| `PUSH2` | 1 | 3 | push location to jump back to where we proceed |
| jump offest | 2 | 0 | points to **first** jump offset (after `JUMPI`) |
| `JUMP` | 1 | 8 | jump back after evaluating `rhs` |
| --------------- | ---- | --- | ----------------------------------------------------------- |
| Total | 12 | 32 | |

So our code will always bloat by at least 12 bytes, and even if the short-circuiting triggers, we still pay for the `PUSH`, the `JUMPI`, and stepping over the subsequent `JUMPDST` for a total of 17 gas, when the alternative can be as cheap as a single `AND` for 1 byte and 3 gas (if we just check a boolean thats already on the stack).

This is particularly unnecessary when checking that a bunch of variables all have their expected values, and where short-circuiting would _at best_ make the failing path cheaper, while always introducing the gas overhead on our precious happy path.

The way to avoid this is using the `eagerAnd` and `eagerOr` utility functions:

```solidity
function eagerAnd(bool lhs, bool rhs) internal pure returns (bool ret) {
assembly ("memory-safe") {
ret := and(lhs, rhs)
}
}
```

Thankfully, while solc is not smart enough to consider the cost/side-effects of evaluating the right hand side before deciding whether to implement short-circuiting or not, but simply _always_ short-circuits, it will at least inline `eagerAnd` and `eagerOr`.
263 changes: 263 additions & 0 deletions src/TypedUnits.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
// SPDX-License-Identifier: Apache 2

pragma solidity ^0.8.19;

type WeiPrice is uint256;

type GasPrice is uint256;

type Gas is uint256;

type Dollar is uint256;

type Wei is uint256;

type LocalNative is uint256;

type TargetNative is uint256;

using {
addWei as +,
subWei as -,
lteWei as <=,
ltWei as <,
gtWei as >,
eqWei as ==,
neqWei as !=
} for Wei global;
using {addTargetNative as +, subTargetNative as -} for TargetNative global;
using {
leLocalNative as <,
leqLocalNative as <=,
neqLocalNative as !=,
addLocalNative as +,
subLocalNative as -
} for LocalNative global;
using {
ltGas as <,
lteGas as <=,
subGas as -
} for Gas global;

using WeiLib for Wei;
using GasLib for Gas;
using DollarLib for Dollar;
using WeiPriceLib for WeiPrice;
using GasPriceLib for GasPrice;

function ltWei(Wei a, Wei b) pure returns (bool) {
return Wei.unwrap(a) < Wei.unwrap(b);
}

function eqWei(Wei a, Wei b) pure returns (bool) {
return Wei.unwrap(a) == Wei.unwrap(b);
}

function gtWei(Wei a, Wei b) pure returns (bool) {
return Wei.unwrap(a) > Wei.unwrap(b);
}

function lteWei(Wei a, Wei b) pure returns (bool) {
return Wei.unwrap(a) <= Wei.unwrap(b);
}

function subWei(Wei a, Wei b) pure returns (Wei) {
return Wei.wrap(Wei.unwrap(a) - Wei.unwrap(b));
}

function addWei(Wei a, Wei b) pure returns (Wei) {
return Wei.wrap(Wei.unwrap(a) + Wei.unwrap(b));
}

function neqWei(Wei a, Wei b) pure returns (bool) {
return Wei.unwrap(a) != Wei.unwrap(b);
}

function ltGas(Gas a, Gas b) pure returns (bool) {
return Gas.unwrap(a) < Gas.unwrap(b);
}

function lteGas(Gas a, Gas b) pure returns (bool) {
return Gas.unwrap(a) <= Gas.unwrap(b);
}

function subGas(Gas a, Gas b) pure returns (Gas) {
return Gas.wrap(Gas.unwrap(a) - Gas.unwrap(b));
}

function addTargetNative(TargetNative a, TargetNative b) pure returns (TargetNative) {
return TargetNative.wrap(TargetNative.unwrap(a) + TargetNative.unwrap(b));
}

function subTargetNative(TargetNative a, TargetNative b) pure returns (TargetNative) {
return TargetNative.wrap(TargetNative.unwrap(a) - TargetNative.unwrap(b));
}

function addLocalNative(LocalNative a, LocalNative b) pure returns (LocalNative) {
return LocalNative.wrap(LocalNative.unwrap(a) + LocalNative.unwrap(b));
}

function subLocalNative(LocalNative a, LocalNative b) pure returns (LocalNative) {
return LocalNative.wrap(LocalNative.unwrap(a) - LocalNative.unwrap(b));
}

function neqLocalNative(LocalNative a, LocalNative b) pure returns (bool) {
return LocalNative.unwrap(a) != LocalNative.unwrap(b);
}

function leLocalNative(LocalNative a, LocalNative b) pure returns (bool) {
return LocalNative.unwrap(a) < LocalNative.unwrap(b);
}

function leqLocalNative(LocalNative a, LocalNative b) pure returns (bool) {
return LocalNative.unwrap(a) <= LocalNative.unwrap(b);
}

library WeiLib {
using {
toDollars,
toGas,
convertAsset,
min,
max,
scale,
unwrap,
asGasPrice,
asTargetNative,
asLocalNative
} for Wei;

function min(Wei x, Wei maxVal) internal pure returns (Wei) {
return x > maxVal ? maxVal : x;
}

function max(Wei x, Wei maxVal) internal pure returns (Wei) {
return x < maxVal ? maxVal : x;
}

function asTargetNative(Wei w) internal pure returns (TargetNative) {
return TargetNative.wrap(Wei.unwrap(w));
}

function asLocalNative(Wei w) internal pure returns (LocalNative) {
return LocalNative.wrap(Wei.unwrap(w));
}

function toDollars(Wei w, WeiPrice price) internal pure returns (Dollar) {
return Dollar.wrap(Wei.unwrap(w) * WeiPrice.unwrap(price));
}

function toGas(Wei w, GasPrice price) internal pure returns (Gas) {
return Gas.wrap(Wei.unwrap(w) / GasPrice.unwrap(price));
}

function scale(Wei w, Gas num, Gas denom) internal pure returns (Wei) {
return Wei.wrap(Wei.unwrap(w) * Gas.unwrap(num) / Gas.unwrap(denom));
}

function unwrap(Wei w) internal pure returns (uint256) {
return Wei.unwrap(w);
}

function asGasPrice(Wei w) internal pure returns (GasPrice) {
return GasPrice.wrap(Wei.unwrap(w));
}

function convertAsset(
Wei w,
WeiPrice fromPrice,
WeiPrice toPrice,
uint32 multiplierNum,
uint32 multiplierDenom,
bool roundUp
) internal pure returns (Wei) {
Dollar numerator = w.toDollars(fromPrice).mul(multiplierNum);
WeiPrice denom = toPrice.mul(multiplierDenom);
Wei res = numerator.toWei(denom, roundUp);
return res;
}
}

library GasLib {
using {toWei, unwrap} for Gas;

function min(Gas x, Gas maxVal) internal pure returns (Gas) {
return x < maxVal ? x : maxVal;
}

function toWei(Gas w, GasPrice price) internal pure returns (Wei) {
return Wei.wrap(w.unwrap() * price.unwrap());
}

function unwrap(Gas w) internal pure returns (uint256) {
return Gas.unwrap(w);
}
}

library DollarLib {
using {toWei, mul, unwrap} for Dollar;

function mul(Dollar a, uint256 b) internal pure returns (Dollar) {
return Dollar.wrap(a.unwrap() * b);
}

function toWei(Dollar w, WeiPrice price, bool roundUp) internal pure returns (Wei) {
return Wei.wrap((w.unwrap() + (roundUp ? price.unwrap() - 1 : 0)) / price.unwrap());
}

function toGas(Dollar w, GasPrice price, WeiPrice weiPrice) internal pure returns (Gas) {
return w.toWei(weiPrice, false).toGas(price);
}

function unwrap(Dollar w) internal pure returns (uint256) {
return Dollar.unwrap(w);
}
}

library WeiPriceLib {
using {mul, unwrap} for WeiPrice;

function mul(WeiPrice a, uint256 b) internal pure returns (WeiPrice) {
return WeiPrice.wrap(a.unwrap() * b);
}

function unwrap(WeiPrice w) internal pure returns (uint256) {
return WeiPrice.unwrap(w);
}
}

library GasPriceLib {
using {unwrap, priceAsWei} for GasPrice;

function priceAsWei(GasPrice w) internal pure returns (Wei) {
return Wei.wrap(w.unwrap());
}

function unwrap(GasPrice w) internal pure returns (uint256) {
return GasPrice.unwrap(w);
}
}

library TargetNativeLib {
using {unwrap, asNative} for TargetNative;

function unwrap(TargetNative w) internal pure returns (uint256) {
return TargetNative.unwrap(w);
}

function asNative(TargetNative w) internal pure returns (Wei) {
return Wei.wrap(TargetNative.unwrap(w));
}
}

library LocalNativeLib {
using {unwrap, asNative} for LocalNative;

function unwrap(LocalNative w) internal pure returns (uint256) {
return LocalNative.unwrap(w);
}

function asNative(LocalNative w) internal pure returns (Wei) {
return Wei.wrap(LocalNative.unwrap(w));
}
}
14 changes: 14 additions & 0 deletions src/Utils.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,17 @@ function reRevert(bytes memory err) pure {
revert(add(err, 32), mload(err))
}
}

//see Optimization.md for rationale on avoiding short-circuiting
function eagerAnd(bool lhs, bool rhs) internal pure returns (bool ret) {
assembly ("memory-safe") {
ret := and(lhs, rhs)
}
}

//see Optimization.md for rationale on avoiding short-circuiting
function eagerOr(bool lhs, bool rhs) internal pure returns (bool ret) {
assembly ("memory-safe") {
ret := or(lhs, rhs)
}
}
Loading

0 comments on commit 21ea424

Please sign in to comment.