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

Change the constant optimizer to make use of PUSH0 #14117

Merged
merged 4 commits into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
8 changes: 8 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1438,6 +1438,14 @@ jobs:
# TODO: temporarily set hardhat stack traces tests to use cancun hardfork
# Remove this when hardhat switch to cancun by default: https://github.com/NomicFoundation/hardhat/issues/4851
sed -i 's/hardfork: "shanghai",/hardfork: "cancun",/' test/internal/hardhat-network/stack-traces/execution.ts
# TODO: Remove these tests because they rely on specific stack sequence which has changed since
# Remove when hardhat properly fix a specific version for the tests: https://github.com/NomicFoundation/hardhat/issues/5443
rm -rf test/internal/hardhat-network/stack-traces/test-files/0_8/revert-without-message/modifiers/call-message/multiple-modifiers-require/
rm -rf test/internal/hardhat-network/stack-traces/test-files/0_8/revert-without-message/modifiers/create-message/multiple-modifiers-require/
rm -rf test/internal/hardhat-network/stack-traces/test-files/0_8/revert-without-message/revert-without-message/within-receive/between-statements/
rm -rf test/internal/hardhat-network/stack-traces/test-files/0_8/revert-without-message/revert-without-message/within-receive/no-other-statements/
rm -rf test/internal/hardhat-network/stack-traces/test-files/0_8/revert-without-message/revert-without-message/within-receive/statement-after/
rm -rf test/internal/hardhat-network/stack-traces/test-files/0_8/revert-without-message/revert-without-message/within-receive/statement-before/
matheusaaguiar marked this conversation as resolved.
Show resolved Hide resolved
pnpm test
- matrix_notify_failure_unless_pr

Expand Down
3 changes: 3 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ Language Features:
Compiler Features:
* Command Line Interface: Do not perform IR optimization when only unoptimized IR is requested.
* Commandline Interface: Add ``--transient-storage-layout`` output.
* Constant Optimizer: Uses ``PUSH0`` if supported by the selected evm version.
* Error Reporting: Unimplemented features are now properly reported as errors instead of being handled as if they were bugs.
* EVM: Support for the EVM version "Prague".
* Peephole Optimizer: ``PUSH0``, when supported, is duplicated explicitly instead of using ``DUP1``.
* Peephole optimizer: Remove identical code snippets that terminate the control flow if they occur one after another.
* SMTChecker: Add CHC engine check for underflow and overflow in unary minus operation.
* SMTChecker: Replace CVC4 as a possible BMC backend with cvc5.
* Standard JSON Interface: Do not perform IR optimization when only unoptimized IR is requested.
Expand Down
4 changes: 2 additions & 2 deletions libevmasm/Assembly.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ unsigned Assembly::codeSize(unsigned subTagSize) const
ret += i.second.size();

for (AssemblyItem const& i: m_items)
ret += i.bytesRequired(tagSize, Precision::Approximate);
ret += i.bytesRequired(tagSize, m_evmVersion, Precision::Approximate);
if (numberEncodingSize(ret) <= tagSize)
return static_cast<unsigned>(ret);
}
Expand Down Expand Up @@ -753,7 +753,7 @@ std::map<u256, u256> const& Assembly::optimiseInternal(

if (_settings.runPeephole)
{
PeepholeOptimiser peepOpt{m_items};
PeepholeOptimiser peepOpt{m_items, m_evmVersion};
while (peepOpt.optimise())
{
count++;
Expand Down
6 changes: 4 additions & 2 deletions libevmasm/AssemblyItem.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -116,15 +116,17 @@ void AssemblyItem::setPushTagSubIdAndTag(size_t _subId, size_t _tag)
setData(data);
}

size_t AssemblyItem::bytesRequired(size_t _addressLength, Precision _precision) const
size_t AssemblyItem::bytesRequired(size_t _addressLength, langutil::EVMVersion _evmVersion, Precision _precision) const
{
switch (m_type)
{
case Operation:
case Tag: // 1 byte for the JUMPDEST
return 1;
case Push:
return 1 + std::max<size_t>(1, numberEncodingSize(data()));
return
1 +
std::max<size_t>((_evmVersion.hasPush0() ? 0 : 1), numberEncodingSize(data()));
case PushSubSize:
case PushProgramSize:
return 1 + 4; // worst case: a 16MB program
Expand Down
7 changes: 4 additions & 3 deletions libevmasm/AssemblyItem.h
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,11 @@ class AssemblyItem

/// @returns an upper bound for the number of bytes required by this item, assuming that
/// the value of a jump tag takes @a _addressLength bytes.
/// @param _evmVersion the EVM version
/// @param _precision Whether to return a precise count (which involves
/// counting immutable references which are only set after
/// a call to `assemble()`) or an approx. count.
size_t bytesRequired(size_t _addressLength, Precision _precision = Precision::Precise) const;
size_t bytesRequired(size_t _addressLength, langutil::EVMVersion _evmVersion, Precision _precision = Precision::Precise) const;
size_t arguments() const;
size_t returnValues() const;
size_t deposit() const { return returnValues() - arguments(); }
Expand Down Expand Up @@ -228,11 +229,11 @@ class AssemblyItem
mutable std::optional<size_t> m_immutableOccurrences;
};

inline size_t bytesRequired(AssemblyItems const& _items, size_t _addressLength, Precision _precision = Precision::Precise)
inline size_t bytesRequired(AssemblyItems const& _items, size_t _addressLength, langutil::EVMVersion _evmVersion, Precision _precision = Precision::Precise)
{
size_t size = 0;
for (AssemblyItem const& item: _items)
size += item.bytesRequired(_addressLength, _precision);
size += item.bytesRequired(_addressLength, _evmVersion, _precision);
return size;
}

Expand Down
95 changes: 66 additions & 29 deletions libevmasm/ConstantOptimiser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ bigint ConstantOptimisationMethod::simpleRunGas(AssemblyItems const& _items, lan
bigint gas = 0;
for (AssemblyItem const& item: _items)
if (item.type() == Push)
gas += GasMeter::runGas(Instruction::PUSH1, _evmVersion);
gas += GasMeter::pushGas(item.data(), _evmVersion);
else if (item.type() == Operation)
{
if (item.instruction() == Instruction::EXP)
Expand All @@ -100,9 +100,9 @@ bigint ConstantOptimisationMethod::dataGas(bytes const& _data) const
return bigint(GasMeter::dataGas(_data, m_params.isCreation, m_params.evmVersion));
}

size_t ConstantOptimisationMethod::bytesRequired(AssemblyItems const& _items)
size_t ConstantOptimisationMethod::bytesRequired(AssemblyItems const& _items, langutil::EVMVersion _evmVersion)
{
return evmasm::bytesRequired(_items, 3, Precision::Approximate); // assume 3 byte addresses
return evmasm::bytesRequired(_items, 3, _evmVersion, Precision::Approximate); // assume 3 byte addresses
}

void ConstantOptimisationMethod::replaceConstants(
Expand Down Expand Up @@ -143,7 +143,7 @@ bigint CodeCopyMethod::gasNeeded() const
// Run gas: we ignore memory increase costs
simpleRunGas(copyRoutine(), m_params.evmVersion) + GasCosts::copyGas,
// Data gas for copy routines: Some bytes are zero, but we ignore them.
bytesRequired(copyRoutine()) * (m_params.isCreation ? GasCosts::txDataNonZeroGas(m_params.evmVersion) : GasCosts::createDataGas),
bytesRequired(copyRoutine(), m_params.evmVersion) * (m_params.isCreation ? GasCosts::txDataNonZeroGas(m_params.evmVersion) : GasCosts::createDataGas),
// Data gas for data itself
dataGas(toBigEndian(m_value))
);
Expand All @@ -153,37 +153,74 @@ AssemblyItems CodeCopyMethod::execute(Assembly& _assembly) const
{
bytes data = toBigEndian(m_value);
assertThrow(data.size() == 32, OptimizerException, "Invalid number encoding.");
AssemblyItems actualCopyRoutine = copyRoutine();
actualCopyRoutine[4] = _assembly.newData(data);
return actualCopyRoutine;
AssemblyItem newPushData = _assembly.newData(data);
return copyRoutine(&newPushData);
}

AssemblyItems const& CodeCopyMethod::copyRoutine()
AssemblyItems CodeCopyMethod::copyRoutine(AssemblyItem* _pushData) const
{
AssemblyItems static copyRoutine{
// constant to be reused 3+ times
u256(0),
if (_pushData)
assertThrow(_pushData->type() == PushData, OptimizerException, "Invalid Assembly Item.");

// back up memory
// mload(0)
Instruction::DUP1,
Instruction::MLOAD,
AssemblyItem dataUsed = _pushData ? *_pushData : AssemblyItem(PushData, u256(1) << 16);

// codecopy(0, <offset>, 32)
u256(32),
AssemblyItem(PushData, u256(1) << 16), // replaced above in actualCopyRoutine[4]
Instruction::DUP4,
Instruction::CODECOPY,
// PUSH0 is cheaper than PUSHn/DUP/SWAP.
if (m_params.evmVersion.hasPush0())
{
// This costs ~29 gas.
AssemblyItems copyRoutine{
// back up memory
// mload(0)
u256(0),
Instruction::MLOAD,

// codecopy(0, <offset>, 32)
u256(32),
dataUsed,
u256(0),
Instruction::CODECOPY,

// mload(0)
u256(0),
Instruction::MLOAD,

// restore original memory
// mstore(0, x)
Instruction::SWAP1,
u256(0),
Instruction::MSTORE
};
return copyRoutine;
}
else
{
// This costs ~33 gas.
AssemblyItems copyRoutine{
// constant to be reused 3+ times
u256(0),

// back up memory
// mload(0)
Instruction::DUP1,
Instruction::MLOAD,

// mload(0)
Instruction::DUP2,
Instruction::MLOAD,
// codecopy(0, <offset>, 32)
u256(32),
dataUsed,
Instruction::DUP4,
Instruction::CODECOPY,

// restore original memory
Instruction::SWAP2,
Instruction::MSTORE
};
return copyRoutine;
// mload(0)
Instruction::DUP2,
Instruction::MLOAD,

// restore original memory
// mstore(0, x)
Instruction::SWAP2,
Instruction::MSTORE
};
return copyRoutine;
}
}

AssemblyItems ComputeMethod::findRepresentation(u256 const& _value)
Expand Down Expand Up @@ -323,7 +360,7 @@ bigint ComputeMethod::gasNeeded(AssemblyItems const& _routine) const
return combineGas(
simpleRunGas(_routine, m_params.evmVersion) + numExps * (GasCosts::expGas + GasCosts::expByteGas(m_params.evmVersion)),
// Data gas for routine: Some bytes are zero, but we ignore them.
bytesRequired(_routine) * (m_params.isCreation ? GasCosts::txDataNonZeroGas(m_params.evmVersion) : GasCosts::createDataGas),
bytesRequired(_routine, m_params.evmVersion) * (m_params.isCreation ? GasCosts::txDataNonZeroGas(m_params.evmVersion) : GasCosts::createDataGas),
0
);
}
4 changes: 2 additions & 2 deletions libevmasm/ConstantOptimiser.h
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ class ConstantOptimisationMethod
static bigint simpleRunGas(AssemblyItems const& _items, langutil::EVMVersion _evmVersion);
/// @returns the gas needed to store the given data literally
bigint dataGas(bytes const& _data) const;
static size_t bytesRequired(AssemblyItems const& _items);
static size_t bytesRequired(AssemblyItems const& _items, langutil::EVMVersion _evmVersion);
/// @returns the combined estimated gas usage taking @a m_params into account.
bigint combineGas(
bigint const& _runGas,
Expand Down Expand Up @@ -123,7 +123,7 @@ class CodeCopyMethod: public ConstantOptimisationMethod
AssemblyItems execute(Assembly& _assembly) const override;

protected:
static AssemblyItems const& copyRoutine();
AssemblyItems copyRoutine(AssemblyItem* _pushData = nullptr) const;
};

/**
Expand Down
16 changes: 10 additions & 6 deletions libevmasm/GasMeter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,8 @@ GasMeter::GasConsumption GasMeter::estimateMax(AssemblyItem const& _item, bool _
switch (_item.type())
{
case Push:
if (m_evmVersion.hasPush0() && _item.data() == 0)
{
gas = runGas(Instruction::PUSH0, m_evmVersion);
break;
}
[[fallthrough]];
gas = pushGas(_item.data(), m_evmVersion);
break;
case PushTag:
case PushData:
case PushSub:
Expand Down Expand Up @@ -290,6 +286,14 @@ unsigned GasMeter::runGas(Instruction _instruction, langutil::EVMVersion _evmVer
util::unreachable();
}

unsigned GasMeter::pushGas(u256 _value, langutil::EVMVersion _evmVersion)
{
return runGas(
(_evmVersion.hasPush0() && _value == u256(0)) ? Instruction::PUSH0 : Instruction::PUSH1,
_evmVersion
);
}

u256 GasMeter::dataGas(bytes const& _data, bool _inCreation, langutil::EVMVersion _evmVersion)
{
bigint gas = 0;
Expand Down
3 changes: 3 additions & 0 deletions libevmasm/GasMeter.h
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,9 @@ class GasMeter
/// change with EVM versions)
static unsigned runGas(Instruction _instruction, langutil::EVMVersion _evmVersion);

/// @returns gas costs for push instructions (may change depending on EVM version)
static unsigned pushGas(u256 _value, langutil::EVMVersion _evmVersion);

/// @returns the gas cost of the supplied data, depending whether it is in creation code, or not.
/// In case of @a _inCreation, the data is only sent as a transaction and is not stored, whereas
/// otherwise code will be stored and have to pay "createDataGas" cost.
Expand Down
16 changes: 8 additions & 8 deletions libevmasm/Inliner.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,10 @@ u256 executionCost(RangeType const& _itemRange, langutil::EVMVersion _evmVersion
}
/// @returns an estimation of the code size in bytes needed for the AssemblyItems in @a _itemRange.
template<typename RangeType>
uint64_t codeSize(RangeType const& _itemRange)
uint64_t codeSize(RangeType const& _itemRange, langutil::EVMVersion _evmVersion)
{
return ranges::accumulate(_itemRange | ranges::views::transform(
[](auto const& _item) { return _item.bytesRequired(2, Precision::Approximate); }
[&](auto const& _item) { return _item.bytesRequired(2, _evmVersion, Precision::Approximate); }
), 0u);
}
/// @returns the tag id, if @a _item is a PushTag or Tag into the current subassembly, std::nullopt otherwise.
Expand Down Expand Up @@ -139,7 +139,7 @@ std::map<size_t, Inliner::InlinableBlock> Inliner::determineInlinableBlocks(Asse
bool Inliner::shouldInlineFullFunctionBody(size_t _tag, ranges::span<AssemblyItem const> _block, uint64_t _pushTagCount) const
{
// Accumulate size of the inline candidate block in bytes (without the return jump).
uint64_t functionBodySize = codeSize(ranges::views::drop_last(_block, 1));
uint64_t functionBodySize = codeSize(ranges::views::drop_last(_block, 1), m_evmVersion);

// Use the number of push tags as approximation of the average number of calls to the function per run.
uint64_t numberOfCalls = _pushTagCount;
Expand Down Expand Up @@ -167,8 +167,8 @@ bool Inliner::shouldInlineFullFunctionBody(size_t _tag, ranges::span<AssemblyIte
);
// Each call site deposits the call site pattern, whereas the jump site pattern and the function itself are deposited once.
bigint uninlinedDepositCost = GasMeter::dataGas(
numberOfCallSites * codeSize(uninlinedCallSitePattern) +
codeSize(uninlinedFunctionPattern) +
numberOfCallSites * codeSize(uninlinedCallSitePattern, m_evmVersion) +
codeSize(uninlinedFunctionPattern, m_evmVersion) +
functionBodySize,
m_isCreation,
m_evmVersion
Expand All @@ -185,7 +185,7 @@ bool Inliner::shouldInlineFullFunctionBody(size_t _tag, ranges::span<AssemblyIte
// the heuristics is optimistic.
if (m_tagsReferencedFromOutside.count(_tag))
inlinedDepositCost += GasMeter::dataGas(
codeSize(uninlinedFunctionPattern) + functionBodySize,
codeSize(uninlinedFunctionPattern, m_evmVersion) + functionBodySize,
m_isCreation,
m_evmVersion
);
Expand Down Expand Up @@ -225,8 +225,8 @@ std::optional<AssemblyItem> Inliner::shouldInline(size_t _tag, AssemblyItem cons
AssemblyItem{Instruction::JUMP},
};
if (
GasMeter::dataGas(codeSize(_block.items), m_isCreation, m_evmVersion) <=
GasMeter::dataGas(codeSize(jumpPattern), m_isCreation, m_evmVersion)
GasMeter::dataGas(codeSize(_block.items, m_evmVersion), m_isCreation, m_evmVersion) <=
GasMeter::dataGas(codeSize(jumpPattern, m_evmVersion), m_isCreation, m_evmVersion)
)
return blockExit;
}
Expand Down
Loading