Skip to content

Commit 4032b42

Browse files
LohannAmxxernestognw
authored
Branchless ternary, min and max methods (#4976)
Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com> Co-authored-by: Ernesto García <ernestognw@gmail.com>
1 parent 60afc99 commit 4032b42

File tree

5 files changed

+66
-7
lines changed

5 files changed

+66
-7
lines changed

.changeset/spotty-falcons-explain.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-solidity': minor
3+
---
4+
5+
`Math`, `SignedMath`: Add a branchless `ternary` function that computes`cond ? a : b` in constant gas cost.

contracts/utils/math/Math.sol

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,18 +73,34 @@ library Math {
7373
}
7474
}
7575

76+
/**
77+
* @dev Branchless ternary evaluation for `a ? b : c`. Gas costs are constant.
78+
*
79+
* IMPORTANT: This function may reduce bytecode size and consume less gas when used standalone.
80+
* However, the compiler may optimize Solidity ternary operations (i.e. `a ? b : c`) to only compute
81+
* one branch when needed, making this function more expensive.
82+
*/
83+
function ternary(bool condition, uint256 a, uint256 b) internal pure returns (uint256) {
84+
unchecked {
85+
// branchless ternary works because:
86+
// b ^ (a ^ b) == a
87+
// b ^ 0 == b
88+
return b ^ ((a ^ b) * SafeCast.toUint(condition));
89+
}
90+
}
91+
7692
/**
7793
* @dev Returns the largest of two numbers.
7894
*/
7995
function max(uint256 a, uint256 b) internal pure returns (uint256) {
80-
return a > b ? a : b;
96+
return ternary(a > b, a, b);
8197
}
8298

8399
/**
84100
* @dev Returns the smallest of two numbers.
85101
*/
86102
function min(uint256 a, uint256 b) internal pure returns (uint256) {
87-
return a < b ? a : b;
103+
return ternary(a < b, a, b);
88104
}
89105

90106
/**
@@ -114,7 +130,7 @@ library Math {
114130
// but the largest value we can obtain is type(uint256).max - 1, which happens
115131
// when a = type(uint256).max and b = 1.
116132
unchecked {
117-
return a == 0 ? 0 : (a - 1) / b + 1;
133+
return SafeCast.toUint(a > 0) * ((a - 1) / b + 1);
118134
}
119135
}
120136

@@ -147,7 +163,7 @@ library Math {
147163

148164
// Make sure the result is less than 2²⁵⁶. Also prevents denominator == 0.
149165
if (denominator <= prod1) {
150-
Panic.panic(denominator == 0 ? Panic.DIVISION_BY_ZERO : Panic.UNDER_OVERFLOW);
166+
Panic.panic(ternary(denominator == 0, Panic.DIVISION_BY_ZERO, Panic.UNDER_OVERFLOW));
151167
}
152168

153169
///////////////////////////////////////////////
@@ -268,7 +284,7 @@ library Math {
268284
}
269285

270286
if (gcd != 1) return 0; // No inverse exists.
271-
return x < 0 ? (n - uint256(-x)) : uint256(x); // Wrap the result if it's negative.
287+
return ternary(x < 0, n - uint256(-x), uint256(x)); // Wrap the result if it's negative.
272288
}
273289
}
274290

contracts/utils/math/SignedMath.sol

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,40 @@
33

44
pragma solidity ^0.8.20;
55

6+
import {SafeCast} from "./SafeCast.sol";
7+
68
/**
79
* @dev Standard signed math utilities missing in the Solidity language.
810
*/
911
library SignedMath {
12+
/**
13+
* @dev Branchless ternary evaluation for `a ? b : c`. Gas costs are constant.
14+
*
15+
* IMPORTANT: This function may reduce bytecode size and consume less gas when used standalone.
16+
* However, the compiler may optimize Solidity ternary operations (i.e. `a ? b : c`) to only compute
17+
* one branch when needed, making this function more expensive.
18+
*/
19+
function ternary(bool condition, int256 a, int256 b) internal pure returns (int256) {
20+
unchecked {
21+
// branchless terinary works because:
22+
// b ^ (a ^ b) == a
23+
// b ^ 0 == b
24+
return b ^ ((a ^ b) * int256(SafeCast.toUint(condition)));
25+
}
26+
}
27+
1028
/**
1129
* @dev Returns the largest of two signed numbers.
1230
*/
1331
function max(int256 a, int256 b) internal pure returns (int256) {
14-
return a > b ? a : b;
32+
return ternary(a > b, a, b);
1533
}
1634

1735
/**
1836
* @dev Returns the smallest of two signed numbers.
1937
*/
2038
function min(int256 a, int256 b) internal pure returns (int256) {
21-
return a < b ? a : b;
39+
return ternary(a < b, a, b);
2240
}
2341

2442
/**

test/utils/math/Math.t.sol

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ import {Test, stdError} from "forge-std/Test.sol";
77
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
88

99
contract MathTest is Test {
10+
function testSelect(bool f, uint256 a, uint256 b) public {
11+
assertEq(Math.ternary(f, a, b), f ? a : b);
12+
}
13+
14+
// MIN & MAX
15+
function testMinMax(uint256 a, uint256 b) public {
16+
assertEq(Math.min(a, b), a < b ? a : b);
17+
assertEq(Math.max(a, b), a > b ? a : b);
18+
}
19+
1020
// CEILDIV
1121
function testCeilDiv(uint256 a, uint256 b) public {
1222
vm.assume(b > 0);

test/utils/math/SignedMath.t.sol

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@ import {Math} from "../../../contracts/utils/math/Math.sol";
88
import {SignedMath} from "../../../contracts/utils/math/SignedMath.sol";
99

1010
contract SignedMathTest is Test {
11+
function testSelect(bool f, int256 a, int256 b) public {
12+
assertEq(SignedMath.ternary(f, a, b), f ? a : b);
13+
}
14+
15+
// MIN & MAX
16+
function testMinMax(int256 a, int256 b) public {
17+
assertEq(SignedMath.min(a, b), a < b ? a : b);
18+
assertEq(SignedMath.max(a, b), a > b ? a : b);
19+
}
20+
1121
// MIN
1222
function testMin(int256 a, int256 b) public {
1323
int256 result = SignedMath.min(a, b);

0 commit comments

Comments
 (0)