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

Branchless ternary, min and max methods #4976

Merged
merged 20 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/spotty-falcons-explain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

Replaces simple ternary operations `cond ? a : b` by a more efficient branchless `choice` function, now supported by `Math.sol` and `SignedMath.sol`.
33 changes: 25 additions & 8 deletions contracts/utils/math/Math.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (utils/math/Math.sol)
// OpenZeppelin Contracts (last updated v5.0.2) (utils/math/Math.sol)
Amxx marked this conversation as resolved.
Show resolved Hide resolved

pragma solidity ^0.8.20;

Expand Down Expand Up @@ -76,15 +76,32 @@ library Math {
/**
* @dev Returns the largest of two numbers.
*/
function max(uint256 a, uint256 b) internal pure returns (uint256) {
return a > b ? a : b;
function max(uint256 a, uint256 b) internal pure returns (uint256 result) {
return choice(a > b, a, b);
}

/**
* @dev Returns the smallest of two numbers.
*/
function min(uint256 a, uint256 b) internal pure returns (uint256) {
return a < b ? a : b;
function min(uint256 a, uint256 b) internal pure returns (uint256 result) {
return choice(a < b, a, b);
}

/**
* @dev If `condition` is true, returns `a`, otherwise returns `b`.
*/
Lohann marked this conversation as resolved.
Show resolved Hide resolved
function choice(bool condition, uint256 a, uint256 b) internal pure returns (uint256) {
Lohann marked this conversation as resolved.
Show resolved Hide resolved
unchecked {
// branchless choice function, works because:
// b ^ (a ^ b) == a
// b ^ 0 == b
//
// This is better than doing `condition ? a : b` for the give reasons:
// - Consumes less gas
// - Constant gas cost regardless the inputs
// - Reduces the final bytecode size
Lohann marked this conversation as resolved.
Show resolved Hide resolved
return b ^ ((a ^ b) * SafeCast.toUint(condition));
}
}

/**
Expand Down Expand Up @@ -114,7 +131,7 @@ library Math {
// but the largest value we can obtain is type(uint256).max - 1, which happens
// when a = type(uint256).max and b = 1.
unchecked {
return a == 0 ? 0 : (a - 1) / b + 1;
return choice(a == 0, 0, (a - 1) / b + 1);
Lohann marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand Down Expand Up @@ -147,7 +164,7 @@ library Math {

// Make sure the result is less than 2²⁵⁶. Also prevents denominator == 0.
if (denominator <= prod1) {
Panic.panic(denominator == 0 ? Panic.DIVISION_BY_ZERO : Panic.UNDER_OVERFLOW);
Panic.panic(choice(denominator == 0, Panic.DIVISION_BY_ZERO, Panic.UNDER_OVERFLOW));
}

///////////////////////////////////////////////
Expand Down Expand Up @@ -268,7 +285,7 @@ library Math {
}

if (gcd != 1) return 0; // No inverse exists.
return x < 0 ? (n - uint256(-x)) : uint256(x); // Wrap the result if it's negative.
return choice(x < 0, n - uint256(-x), uint256(x)); // Wrap the result if it's negative.
}
}

Expand Down
23 changes: 21 additions & 2 deletions contracts/utils/math/SignedMath.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

pragma solidity ^0.8.20;

import {SafeCast} from "./SafeCast.sol";

/**
* @dev Standard signed math utilities missing in the Solidity language.
*/
Expand All @@ -11,14 +13,14 @@ library SignedMath {
* @dev Returns the largest of two signed numbers.
*/
function max(int256 a, int256 b) internal pure returns (int256) {
return a > b ? a : b;
return choice(a > b, a, b);
}

/**
* @dev Returns the smallest of two signed numbers.
*/
function min(int256 a, int256 b) internal pure returns (int256) {
return a < b ? a : b;
return choice(a < b, a, b);
}

/**
Expand Down Expand Up @@ -47,4 +49,21 @@ library SignedMath {
return uint256((n + mask) ^ mask);
}
}

/**
* @dev If `condition` is true, returns `a`, otherwise returns `b`.
*/
function choice(bool condition, int256 a, int256 b) internal pure returns (int256) {
unchecked {
// branchless choice function, works because:
// b ^ (a ^ b) == a
// b ^ 0 == b
//
// This is better than doing `condition ? a : b` for the give reasons:
// - Consumes less gas
// - Constant gas cost regardless the inputs
// - Reduces the final bytecode size
return b ^ ((a ^ b) * int256(SafeCast.toUint(condition)));
}
}
}
6 changes: 6 additions & 0 deletions test/utils/math/Math.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ import {Test, stdError} from "forge-std/Test.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";

contract MathTest is Test {
// MIN & MAX
function testMinMax(uint256 a, uint256 b) public {
assertEq(Math.min(a, b), a < b ? a : b);
assertEq(Math.max(a, b), a > b ? a : b);
}

// CEILDIV
function testCeilDiv(uint256 a, uint256 b) public {
vm.assume(b > 0);
Expand Down
Loading