`memory-safe` Assembly Worsens Optimizations
- Dominant language
- C++
- Stars
- 25.7k
- Forks
- 6.2k
- Avg merge
- 1d 11h
- Merged PRs (30d)
- 21
Description
I did my best to ensure that this issue has not already been reported. I apologise if I missed an existing issue.
## Description
I cam across some specific cases where tagging assembly as `memory-safe` is detrimental to gas optimizations. In particular, the attached case led to ~16.6% increased gas consumption where tagging assembly as `memory-safe`.
Code listing
```solidity
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity ^0.8.20;
contract Base64Url {
function encode(bytes32 input) private pure returns (string memory output) {
assembly ("memory-safe") {
output := mload(0x40)
mstore(0x40, add(output, 96))
mstore(output, 43)
// Write the base-64 lookup table to the scratch space, simplifying the copy code. Note
// that we write the prefix with `MSTORE` as Solidity does not have support for
// `CODECOPY` from bytes or string constants in inline assembly.
mstore(0, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef")
mstore(32, "ghijklmnopqrstuvwxyz0123456789-_")
let ptr := add(output, 32)
for {
let shift := 250
} sgt(shift, 0) {
shift := sub(shift, 6)
} {
mstore8(ptr, byte(0, mload(and(0x3f, shr(shift, input)))))
ptr := add(ptr, 1)
}
mstore8(ptr, byte(0, mload(and(0x3f, shl(2, input)))))
}
}
function bench() external view returns (uint256 gas, bytes32 hash) {
bytes32 input = keccak256("test");
gas = gasleft();
string memory output = encode(input);
assembly ("memory-safe") {
pop(staticcall(gas(), 0x2, output, mload(output), 0, 32))
hash := mload(0)
}
bytes memory data = abi.encode(hash);
assembly ("memory-safe") {
pop(staticcall(gas(), 0x2, data, mload(data), 0, 32))
hash := mload(0)
}
gas = gas - gasleft();
}
}
```
In this particular case, removing the `memory-safe` tag from any of the assembly blocks with result in much more optimal code. From what I understand, having a memory unsafe block will cause the compiler to disable a class of optimization, of them one particular one which is causing it to generate non-optimal code.
I analyzed the compiler output, and it appears to be generating a lot of additional `DUP`, `SWAP` and `POP` stack shuffling inside the `for` loop when all the optimizations are enabled with the `memory-safe` tag, and, because the `for` loop iterates 42 times, causes the large gas discrepancy. Additionally, it is worth noting that the following two `staticcall`s to the SHA-256 precompiles are crucial to confusing the optimizer.
Some other note worthy findings that support that the optimizer is messing up the stack shuffling in this case are:
- The stack shuffling issue is **specific** to the two `staticcall`s to the SHA-256 precompile that follow. In particular, if you remove any of them, the problem is no longer observed and you get the expected behaviour of code with `memory-safe` assembly tags being slightly more performant than potentially memory unsafe assembly.
- If you force the function to not be inlined (by having it be called by more than one `external` function for example), then the bug goes away, and the compiler generates optimal code again. So really the issue is the assembly block followed by the two `staticcall`s.
> (Also on an unrelated note, I noticed that it is compiling `sub(shift, 6)` as `add(shift, 0xffff...ffa` which is 31 more code bytes and also seems like a minor bug...)
## Environment
- Compiler version: 0.8.24 (also checked in earlier versions down to 0.8.20 inclusive)
- Target EVM version (as per compiler settings): default
- Framework/IDE (e.g. Truffle or Remix): Remix/Hardhat
- EVM execution environment / backend / blockchain client: Remix/Hardhat
- Operating system: Linux
## Steps to Reproduce
See code listing :point_up:. Compiler settings:
```json
{
"optimizer": {
"enabled": true,
"runs": 100000
},
"viaIR": true
}
```
Contributor guide
Research direction
Start with the Solidity code listing and reproduce it using the stated viaIR and optimizer settings in compiler version 0.8.24. Compare compiler output and gas for the memory-safe blocks, especially the loop and following SHA-256 staticcalls, against versions with the tag removed. Done means the reported stack-shuffling regression and gas increase are addressed without breaking the reproduction.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- solidity
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100