Skip to content

Commit

Permalink
Merge pull request #14117 from ethereum/push0-optimisations
Browse files Browse the repository at this point in the history
Change the constant optimizer to make use of `PUSH0`
  • Loading branch information
ekpyron authored Aug 5, 2024
2 parents 38088a1 + a500a63 commit 5dbaa13
Show file tree
Hide file tree
Showing 216 changed files with 868 additions and 655 deletions.
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/
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

0 comments on commit 5dbaa13

Please sign in to comment.