Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
* `Arrays`: Add `unsafeAccess` functions that allow reading and writing to an element in a storage array bypassing Solidity's "out-of-bounds" check. ([#3589](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3589))
* `Strings`: optimize `toString`. ([#3573](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3573))
* `Ownable2Step`: extension of `Ownable` that makes the ownership transfers a two step process. ([#3620](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3620))
* `Math`: Add `log2`, `log10` and `log256`. ([#3670](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3670))

### Breaking changes

Expand Down
12 changes: 12 additions & 0 deletions contracts/mocks/MathMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,16 @@ contract MathMock {
function sqrt(uint256 a, Math.Rounding direction) public pure returns (uint256) {
return Math.sqrt(a, direction);
}

function log2(uint256 a, Math.Rounding direction) public pure returns (uint256) {
return Math.log2(a, direction);
}

function log10(uint256 a, Math.Rounding direction) public pure returns (uint256) {
return Math.log10(a, direction);
}

function log256(uint256 a, Math.Rounding direction) public pure returns (uint256) {
return Math.log256(a, direction);
}
}
64 changes: 4 additions & 60 deletions contracts/utils/Strings.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

pragma solidity ^0.8.0;

import "./math/Math.sol";

/**
* @dev String operations.
*/
Expand All @@ -15,39 +17,7 @@ library Strings {
*/
function toString(uint256 value) internal pure returns (string memory) {
unchecked {
uint256 length = 1;

// compute log10(value), and add it to length
uint256 valueCopy = value;
if (valueCopy >= 10**64) {
valueCopy /= 10**64;
length += 64;
}
if (valueCopy >= 10**32) {
valueCopy /= 10**32;
length += 32;
}
if (valueCopy >= 10**16) {
valueCopy /= 10**16;
length += 16;
}
if (valueCopy >= 10**8) {
valueCopy /= 10**8;
length += 8;
}
if (valueCopy >= 10**4) {
valueCopy /= 10**4;
length += 4;
}
if (valueCopy >= 10**2) {
valueCopy /= 10**2;
length += 2;
}
if (valueCopy >= 10**1) {
length += 1;
}
// now, length is log10(value) + 1

uint256 length = Math.log10(value) + 1;
string memory buffer = new string(length);
uint256 ptr;
/// @solidity memory-safe-assembly
Expand All @@ -72,33 +42,7 @@ library Strings {
*/
function toHexString(uint256 value) internal pure returns (string memory) {
unchecked {
uint256 length = 1;

// compute log256(value), and add it to length
uint256 valueCopy = value;
if (valueCopy >= 1 << 128) {
valueCopy >>= 128;
length += 16;
}
if (valueCopy >= 1 << 64) {
valueCopy >>= 64;
length += 8;
}
if (valueCopy >= 1 << 32) {
valueCopy >>= 32;
length += 4;
}
if (valueCopy >= 1 << 16) {
valueCopy >>= 16;
length += 2;
}
if (valueCopy >= 1 << 8) {
valueCopy >>= 8;
length += 1;
}
// now, length is log256(value) + 1

return toHexString(value, length);
return toHexString(value, Math.log256(value) + 1);
}
}

Expand Down
172 changes: 143 additions & 29 deletions contracts/utils/math/Math.sol
Original file line number Diff line number Diff line change
Expand Up @@ -167,35 +167,7 @@ library Math {
// This gives `2**k < a <= 2**(k+1)` → `2**(k/2) <= sqrt(a) < 2 ** (k/2+1)`.
// Using an algorithm similar to the msb computation, we are able to compute `result = 2**(k/2)` which is a
// good first approximation of `sqrt(a)` with at least 1 correct bit.
uint256 result = 1;
uint256 x = a;
if (x >> 128 > 0) {
x >>= 128;
result <<= 64;
}
if (x >> 64 > 0) {
x >>= 64;
result <<= 32;
}
if (x >> 32 > 0) {
x >>= 32;
result <<= 16;
}
if (x >> 16 > 0) {
x >>= 16;
result <<= 8;
}
if (x >> 8 > 0) {
x >>= 8;
result <<= 4;
}
if (x >> 4 > 0) {
x >>= 4;
result <<= 2;
}
if (x >> 2 > 0) {
result <<= 1;
}
uint256 result = 1 << (log2(a) / 2);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can replace 1 << (log2(a) / 2) by 1 << (log2(a) >> 2).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is interresting, but the correct formula would be 1 << (log2(a) >> 1)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohh, sorry. It >>1.

Copy link
Contributor

@frangio frangio Sep 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before changing that I would check if Solidity does not make that optimization on its own.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tested both. using >> 1 saves 54 gas compared to / 2
(0.8.16 with optimizations)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Amxx Because It happen due to solidity checking underflow that. But When we are use shift operator it bypass that check.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overflow check make sens for +, - and * ... not for /
The only reasonable check is that we don't divide by 0 ... but there is no reason to check that here.


// At this point `result` is an estimation with one bit of precision. We know the true value is a uint128,
// since it is the square root of a uint256. Newton's method converges quadratically (precision doubles at
Expand Down Expand Up @@ -223,4 +195,146 @@ library Math {
}
return result;
}

/**
* @dev Return the log in base 2, rounded down, of a positive value.
* Returns 0 if given 0.
*/
function log2(uint256 value) internal pure returns (uint256) {
uint256 result = 0;
if (value >> 128 > 0) {
value >>= 128;
result += 128;
}
if (value >> 64 > 0) {
value >>= 64;
result += 64;
}
if (value >> 32 > 0) {
value >>= 32;
result += 32;
}
if (value >> 16 > 0) {
value >>= 16;
result += 16;
}
if (value >> 8 > 0) {
value >>= 8;
result += 8;
}
if (value >> 4 > 0) {
value >>= 4;
result += 4;
}
if (value >> 2 > 0) {
value >>= 2;
result += 2;
}
if (value >> 1 > 0) {
result += 1;
}
return result;
}

/**
* @dev Return the log in base 2, following the selected rounding direction, of a positive value.
* Returns 0 if given 0.
*/
function log2(uint256 value, Rounding rounding) internal pure returns (uint256) {
uint256 result = log2(value);
if (rounding == Rounding.Up && 1 << result < value) {
result += 1;
}
return result;
}

/**
* @dev Return the log in base 10, rounded down, of a positive value.
* Returns 0 if given 0.
*/
function log10(uint256 value) internal pure returns (uint256) {
uint256 result = 0;
if (value >= 10**64) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Amxx you can replace >= by > and save more gas.

Example:

value >= 10**64 == value > 10**64-1

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Amxx what about this one?

Copy link
Collaborator Author

@Amxx Amxx Sep 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested both, and this change makes it more expensive :/

it looks like the compiler does the -1 operation at runtime

value /= 10**64;
result += 64;
}
if (value >= 10**32) {
value /= 10**32;
result += 32;
}
if (value >= 10**16) {
value /= 10**16;
result += 16;
}
if (value >= 10**8) {
value /= 10**8;
result += 8;
}
if (value >= 10**4) {
value /= 10**4;
result += 4;
}
if (value >= 10**2) {
value /= 10**2;
result += 2;
}
if (value >= 10**1) {
result += 1;
}
return result;
}

/**
* @dev Return the log in base 10, following the selected rounding direction, of a positive value.
* Returns 0 if given 0.
*/
function log10(uint256 value, Rounding rounding) internal pure returns (uint256) {
uint256 result = log10(value);
if (rounding == Rounding.Up && 10**result < value) {
result += 1;
}
return result;
}

/**
* @dev Return the log in base 256, rounded down, of a positive value.
* Returns 0 if given 0.
*
* Adding one to the result gives the number of pairs of hex symbols needed to represent `value` as a hex string.
*/
function log256(uint256 value) internal pure returns (uint256) {
uint256 result = 0;
if (value >> 128 > 0) {
value >>= 128;
result += 16;
}
if (value >> 64 > 0) {
value >>= 64;
result += 8;
}
if (value >> 32 > 0) {
value >>= 32;
result += 4;
}
if (value >> 16 > 0) {
value >>= 16;
result += 2;
}
if (value >> 8 > 0) {
result += 1;
}
return result;
}

/**
* @dev Return the log in base 10, following the selected rounding direction, of a positive value.
* Returns 0 if given 0.
*/
function log256(uint256 value, Rounding rounding) internal pure returns (uint256) {
uint256 result = log256(value);
if (rounding == Rounding.Up && 1 << (result * 8) < value) {
result += 1;
}
return result;
}
}
Loading