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

Add log2, log10 and log256 functions #3670

Merged
merged 9 commits into from
Sep 7, 2022
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.
Jump to
Jump to file
Failed to load files.
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;
}
}