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

V2 optimize #55

Merged
merged 26 commits into from
Aug 10, 2023
Merged

V2 optimize #55

merged 26 commits into from
Aug 10, 2023

Conversation

Vectorized
Copy link

@Vectorized Vectorized commented Aug 7, 2023

Contracts look very efficient overall.

Some minor optimizations.

@0xfoobar
Copy link
Collaborator

0xfoobar commented Aug 7, 2023

Gas diff GH action seems broken on external contributors, can you manually run

forge test --match-contract GasBenchmark --gas-report > gasbenchmark10mil and commit that for comparison?

forge test --match-contract HashBenchmark --gas-report > hashbenchmark10mil is also useful

assembly {
from := and(firstPacked, CLEAN_ADDRESS)
to := and(secondPacked, CLEAN_ADDRESS)
contract_ := or(shr(64, and(firstPacked, CLEAN_PACKED8_BYTES_ADDRESS)), shr(160, secondPacked))
contract_ := or(shr(64, xor(firstPacked, from)), shr(160, secondPacked))
Copy link
Author

Choose a reason for hiding this comment

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

Save some deployment bytecode. Same runtime gas performance.

@0xfoobar
Copy link
Collaborator

0xfoobar commented Aug 7, 2023

We should also use either consistent decimal or consistent hex across all code for pointer location addressing, @mireynolds would love to hear your thoughts here

src/DelegateRegistry.sol Outdated Show resolved Hide resolved
@0xfoobar
Copy link
Collaborator

0xfoobar commented Aug 7, 2023

"Inline assembly that neither involves any operations that access memory nor assigns to any Solidity variables in memory is automatically considered memory-safe and does not need to be annotated." -> Might make sense to leave blocks where this applies unannotated and let the compiler take care of it. Does this apply to any of the new tags @Vectorized ?

Actually on further thought probably good to be as explicit & descriptive as possible everywhere

@mireynolds
Copy link
Contributor

We should also use either consistent decimal or consistent hex across all code for pointer location addressing, @mireynolds would love to hear your thoughts here

Think we should be using decimals consistently in assembly blocks as it makes the code more readable and understandable. It's important to note that most people reading the code are going to familiar with decimal arithmetic rather than hex arithmetic - so using hex makes it harder for others to verify what the code does.

@mireynolds
Copy link
Contributor

"Inline assembly that neither involves any operations that access memory nor assigns to any Solidity variables in memory is automatically considered memory-safe and does not need to be annotated." -> Might make sense to leave blocks where this applies unannotated and let the compiler take care of it. Does this apply to any of the new tags @Vectorized ?

Actually on further thought probably good to be as explicit & descriptive as possible everywhere

We should only be marking assembly blocks as memory safe when it is appropriate: for an assembly block that uses memory operations according to solidity's safe memory rules. Marking assembly blocks as memory safe when they do not use memory operations makes no sense for explicitness and descriptiveness, and it is not convention to do that. If anything, it demonstrates that we don't understand the use of the notation and automatic treatment of assembly blocks which contain no memory operations.

Copy link
Contributor

@mireynolds mireynolds left a comment

Choose a reason for hiding this comment

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

Really like the ideas behind most of these optimisations. Most the issues I've raised can be resolved with minimal additional gas I think. The general theme here is we can skip compiler checks where appropriate / more efficient, but we can't make assumptions about compiler behaviour when it comes to memory management and presence of dirty bits.

src/DelegateRegistry.sol Show resolved Hide resolved
src/libraries/RegistryHashes.sol Show resolved Hide resolved
src/libraries/RegistryHashes.sol Show resolved Hide resolved
src/libraries/RegistryHashes.sol Show resolved Hide resolved
src/libraries/RegistryHashes.sol Show resolved Hide resolved
valid = _validateDelegation(Hashes.allLocation(from, rights, to), from) || _validateDelegation(Hashes.contractLocation(from, rights, to, contract_), from)
|| _validateDelegation(Hashes.erc721Location(from, rights, to, tokenId, contract_), from);
}
assembly ("memory-safe") {
mstore(0x00, valid)
Copy link
Contributor

Choose a reason for hiding this comment

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

Again, assumption of clean bits.

src/libraries/RegistryOps.sol Show resolved Hide resolved
/// @dev `x & y`.
function and(bool x, bool y) internal pure returns (bool z) {
assembly ("memory-safe") {
z := and(x, y)
Copy link
Contributor

Choose a reason for hiding this comment

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

Assumption about clean bits here on the return value.

/// @dev `x | y`.
function or(bool x, bool y) internal pure returns (bool z) {
assembly ("memory-safe") {
z := or(x, y)
Copy link
Contributor

Choose a reason for hiding this comment

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

Similar to above, assumption of clean bits on the return value.

@@ -52,7 +52,7 @@ library RegistryStorage {
assembly {
from := and(firstPacked, CLEAN_ADDRESS)
to := and(secondPacked, CLEAN_ADDRESS)
contract_ := or(shr(64, and(firstPacked, CLEAN_PACKED8_BYTES_ADDRESS)), shr(160, secondPacked))
contract_ := or(shr(64, xor(firstPacked, from)), shr(160, secondPacked))
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we need to stick with a version of this that doesn't result in dirty bits in contract_. We can't assume firstPacked is cleaned of the upper 8 bytes and return it uncleaned.

@0xfoobar 0xfoobar requested a review from mireynolds August 9, 2023 14:03
@Vectorized
Copy link
Author

Thanks for the great feedback!

Please feel free to change anything to fit. :)

I'll continue focusing on other work now, will check back occasionally.

Copy link
Contributor

@mireynolds mireynolds left a comment

Choose a reason for hiding this comment

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

Mostly resolved from what I can tell except for the assumption of clean bits on bools could lead to unexpected behaviour with the iszero operations. Still the question of whether the global lock on memory optimization (as a result of memory usage outside the Solidity memory model in the assembly blocks) in a library that could be used by contracts with high memory usage or lots of local variables that might benefit from memory / stack optimisation.

if (rights != "" && !valid) valid = _validateDelegation(Hashes.allLocation(from, rights, to), from);
if (!Ops.or(rights == "", valid)) valid = _validateDelegation(Hashes.allLocation(from, rights, to), from);
assembly ("memory-safe") {
mstore(0x00, iszero(iszero(valid)))
Copy link
Contributor

Choose a reason for hiding this comment

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

This still assumes valid is free of dirty bits, which the compiler doesn't guarantee. A valid with dirty bits will incorrectly return false under iszero even if the last 8 bits are set to zero. If we're going to use assembly here, we need to clean valid, then store it in memory for return. Compiler will do this under the hood, so if we want to skip a bit of gas on the return we need to ensure it's going to give the same result as if there were no assembly block overriding the return of valid.

valid = _validateDelegation(Hashes.allLocation(from, rights, to), from) || _validateDelegation(Hashes.contractLocation(from, rights, to, contract_), from);
}
assembly ("memory-safe") {
mstore(0x00, iszero(iszero(valid)))
Copy link
Contributor

Choose a reason for hiding this comment

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

See above.

valid = _validateDelegation(Hashes.allLocation(from, rights, to), from) || _validateDelegation(Hashes.contractLocation(from, rights, to, contract_), from)
|| _validateDelegation(Hashes.erc721Location(from, rights, to, tokenId, contract_), from);
}
assembly ("memory-safe") {
mstore(0x00, iszero(iszero(valid)))
Copy link
Contributor

Choose a reason for hiding this comment

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

See above.

/// @dev `x & y`.
function and(bool x, bool y) internal pure returns (bool z) {
assembly {
z := iszero(iszero(and(x, y)))
Copy link
Contributor

Choose a reason for hiding this comment

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

Similar here, unexpected behaviour here if x and y contain dirty bits (which the compiler doesn't guarantee).

Copy link
Author

Choose a reason for hiding this comment

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

The truthyness of booleans in Solidity uses all 256 bits.

/// @dev `x | y`.
function or(bool x, bool y) internal pure returns (bool z) {
assembly {
z := iszero(iszero(or(x, y)))
Copy link
Contributor

Choose a reason for hiding this comment

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

Similar here, unexpected behaviour with dirty bits.

@0xfoobar
Copy link
Collaborator

0xfoobar commented Aug 9, 2023

Reminder to run forge fmt so the GH Action will progress to the test suite

function and(bool x, bool y) internal pure returns (bool z) {
assembly {
// Any non-zero 256 bit word is true, else false.
z := and(iszero(iszero(x)), iszero(iszero(y)))
Copy link
Collaborator

Choose a reason for hiding this comment

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

This double iszero() is incorrect, any dirty upper bits will cause iszero(iszero(x)) to evaluate to 1, even if the lowest bit is 0. So in best case (no dirty upper bits) it's a no-op, and in worst case (dirty upper bits) it can return the incorrect value. Should be removed & replaced with a proper bitshift or other cleaning method.

function or(bool x, bool y) internal pure returns (bool z) {
assembly {
// Any non-zero 256 bit word is true, else false.
z := or(iszero(iszero(x)), iszero(iszero(y)))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Similar incorrect double iszero()

Copy link
Author

@Vectorized Vectorized Aug 10, 2023

Choose a reason for hiding this comment

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

Any 256 bit word with one or more non-zero bit is considered true in regular Solidity. Bools are given special treatment.

Copy link
Author

@Vectorized Vectorized Aug 10, 2023

Choose a reason for hiding this comment

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

Ok, the docs doesn’t explicitly state this rule. But in the compiler, any non-zero 256 bit word is defined as true. There’s no concept of “dirty higher order bits” for bool.

The compiler uses this definition to have a simplification rule that converts a iszero(iszero(b)) to b, if used in the condition for a JUMPI. Here, b can be any bool expression.

https://github.com/ethereum/solidity/blob/3edf91adc8eddd76fe4781698f15c6f1a8a8b355/libevmasm/PeepholeOptimiser.cpp#L294

Copy link
Collaborator

Choose a reason for hiding this comment

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

Looks like the docs explicitly state you can't be sure upper bits will be zero.

"If you access variables of a type that spans less than 256 bits (for example uint64, address, or bytes16), you cannot make any assumptions about bits not part of the encoding of the type. Especially, do not assume them to be zero. To be safe, always clear the data properly before you use it in a context where this is important"

https://docs.soliditylang.org/en/latest/assembly.html#access-to-external-variables-functions-and-libraries

@0xfoobar 0xfoobar changed the base branch from v2 to gasoptvect August 10, 2023 13:50
@0xfoobar
Copy link
Collaborator

Merging into branch on this repo so we can more easily push the latest nits

@0xfoobar 0xfoobar merged commit c52831d into delegatexyz:gasoptvect Aug 10, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants