-
Notifications
You must be signed in to change notification settings - Fork 55
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
V2 optimize #55
Conversation
Gas diff GH action seems broken on external contributors, can you manually run
|
src/libraries/RegistryStorage.sol
Outdated
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)) |
There was a problem hiding this comment.
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.
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 |
"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 |
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. |
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. |
There was a problem hiding this 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
Outdated
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) |
There was a problem hiding this comment.
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
Outdated
/// @dev `x & y`. | ||
function and(bool x, bool y) internal pure returns (bool z) { | ||
assembly ("memory-safe") { | ||
z := and(x, y) |
There was a problem hiding this comment.
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.
src/libraries/RegistryOps.sol
Outdated
/// @dev `x | y`. | ||
function or(bool x, bool y) internal pure returns (bool z) { | ||
assembly ("memory-safe") { | ||
z := or(x, y) |
There was a problem hiding this comment.
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.
src/libraries/RegistryStorage.sol
Outdated
@@ -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)) |
There was a problem hiding this comment.
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.
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. |
There was a problem hiding this 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))) |
There was a problem hiding this comment.
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))) |
There was a problem hiding this comment.
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))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See above.
src/libraries/RegistryOps.sol
Outdated
/// @dev `x & y`. | ||
function and(bool x, bool y) internal pure returns (bool z) { | ||
assembly { | ||
z := iszero(iszero(and(x, y))) |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
src/libraries/RegistryOps.sol
Outdated
/// @dev `x | y`. | ||
function or(bool x, bool y) internal pure returns (bool z) { | ||
assembly { | ||
z := iszero(iszero(or(x, y))) |
There was a problem hiding this comment.
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.
Reminder to run |
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))) |
There was a problem hiding this comment.
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))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar incorrect double iszero()
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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"
Merging into branch on this repo so we can more easily push the latest nits |
Contracts look very efficient overall.
Some minor optimizations.