/* This file is part of solidity. solidity is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. solidity is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with solidity. If not, see . */ // SPDX-License-Identifier: GPL-3.0 /** * @file Inliner.cpp * Inlines small code snippets by replacing JUMP with a copy of the code jumped to. */ #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; using namespace solidity; using namespace solidity::evmasm; namespace { /// @returns an estimation of the runtime gas cost of the AsssemblyItems in @a _itemRange. template u256 executionCost(RangeType const& _itemRange, langutil::EVMVersion _evmVersion) { GasMeter gasMeter{std::make_shared(), _evmVersion}; auto gasConsumption = ranges::accumulate(_itemRange | ranges::views::transform( [&gasMeter](auto const& _item) { return gasMeter.estimateMax(_item, false); } ), GasMeter::GasConsumption()); if (gasConsumption.isInfinite) return numeric_limits::max(); else return gasConsumption.value; } /// @returns an estimation of the code size in bytes needed for the AssemblyItems in @a _itemRange. template uint64_t codeSize(RangeType const& _itemRange) { return ranges::accumulate(_itemRange | ranges::views::transform( [](auto const& _item) { return _item.bytesRequired(2); } ), 0u); } /// @returns the tag id, if @a _item is a PushTag or Tag into the current subassembly, nullopt otherwise. optional getLocalTag(AssemblyItem const& _item) { if (_item.type() != PushTag && _item.type() != Tag) return nullopt; auto [subId, tag] = _item.splitForeignPushTag(); if (subId != numeric_limits::max()) return nullopt; return tag; } } bool Inliner::isInlineCandidate(size_t _tag, ranges::span _items) const { assertThrow(_items.size() > 0, OptimizerException, ""); // Only consider blocks that end in a JUMP for now. This can e.g. be extended to include transaction terminating // instructions as well in the future. if (_items.back() != Instruction::JUMP) return false; // Never inline tags that reference themselves. for (AssemblyItem const& item: _items) if (item.type() == PushTag) if (getLocalTag(item) == _tag) return false; return true; } map Inliner::determineInlinableBlocks(AssemblyItems const& _items) const { std::map> inlinableBlockItems; std::map numPushTags; std::optional lastTag; for (auto&& [index, item]: _items | ranges::views::enumerate) { // The number of PushTags approximates the number of calls to a block. if (item.type() == PushTag) if (optional tag = getLocalTag(item)) ++numPushTags[*tag]; // We can only inline blocks with straight control flow that end in a jump. // Using breaksCSEAnalysisBlock will hopefully allow the return jump to be optimized after inlining. if (lastTag && SemanticInformation::breaksCSEAnalysisBlock(item, false)) { ranges::span block = _items | ranges::views::slice(*lastTag + 1, index + 1); if (optional tag = getLocalTag(_items[*lastTag])) if (isInlineCandidate(*tag, block)) inlinableBlockItems[*tag] = block; lastTag.reset(); } if (item.type() == Tag) { assertThrow(getLocalTag(item), OptimizerException, ""); lastTag = index; } } // Store the number of PushTags alongside the assembly items and discard tags that are never pushed. map result; for (auto&& [tag, items]: inlinableBlockItems) if (uint64_t const* numPushes = util::valueOrNullptr(numPushTags, tag)) result.emplace(tag, InlinableBlock{items, *numPushes}); return result; } bool Inliner::shouldInlineFullFunctionBody(size_t _tag, ranges::span _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)); // Use the number of push tags as approximation of the average number of calls to the function per run. uint64_t numberOfCalls = _pushTagCount; // Also use the number of push tags as approximation of the number of call sites to the function. uint64_t numberOfCallSites = _pushTagCount; static AssemblyItems const uninlinedCallSitePattern = { AssemblyItem{PushTag}, AssemblyItem{PushTag}, AssemblyItem{Instruction::JUMP}, AssemblyItem{Tag} }; static AssemblyItems const uninlinedFunctionPattern = { AssemblyItem{Tag}, // Actual function body of size functionBodySize. Handled separately below. AssemblyItem{Instruction::JUMP} }; // Both the call site and jump site pattern is executed for each call. // Since the function body has to be executed equally often both with and without inlining, // it can be ignored. bigint uninlinedExecutionCost = numberOfCalls * ( executionCost(uninlinedCallSitePattern, m_evmVersion) + executionCost(uninlinedFunctionPattern, m_evmVersion) ); // 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) + functionBodySize, m_isCreation, m_evmVersion ); // When inlining the execution cost beyond the actual function execution is zero, // but for each call site a copy of the function is deposited. bigint inlinedDepositCost = GasMeter::dataGas( numberOfCallSites * functionBodySize, m_isCreation, m_evmVersion ); // If the block is referenced from outside the current subassembly, the original function cannot be removed. // Note that the function also cannot always be removed, if it is not referenced from outside, but in that case // the heuristics is optimistic. if (m_tagsReferencedFromOutside.count(_tag)) inlinedDepositCost += GasMeter::dataGas( uninlinedFunctionPattern.size() + functionBodySize, m_isCreation, m_evmVersion ); // If the estimated runtime cost over the lifetime of the contract plus the deposit cost in the uninlined case // exceed the inlined deposit costs, it is beneficial to inline. if (bigint(m_runs) * uninlinedExecutionCost + uninlinedDepositCost > inlinedDepositCost) return true; return false; } optional Inliner::shouldInline(size_t _tag, AssemblyItem const& _jump, InlinableBlock const& _block) const { AssemblyItem exitJump = _block.items.back(); assertThrow(_jump == Instruction::JUMP && exitJump == Instruction::JUMP, OptimizerException, ""); if ( _jump.getJumpType() == AssemblyItem::JumpType::IntoFunction && exitJump.getJumpType() == AssemblyItem::JumpType::OutOfFunction ) return shouldInlineFullFunctionBody(_tag, _block.items, _block.pushTagCount) ? make_optional(AssemblyItem::JumpType::Ordinary) : nullopt; return nullopt; } void Inliner::optimise() { std::map inlinableBlocks = determineInlinableBlocks(m_items); if (inlinableBlocks.empty()) return; AssemblyItems newItems; for (auto it = m_items.begin(); it != m_items.end(); ++it) { AssemblyItem const& item = *it; if (next(it) != m_items.end()) { AssemblyItem const& nextItem = *next(it); if (item.type() == PushTag && nextItem == Instruction::JUMP) { if (optional tag = getLocalTag(item)) if (auto* inlinableBlock = util::valueOrNullptr(inlinableBlocks, *tag)) if (auto exitJumpType = shouldInline(*tag, nextItem, *inlinableBlock)) { newItems += inlinableBlock->items; newItems.back().setJumpType(*exitJumpType); // We are removing one push tag to the block we inline. --inlinableBlock->pushTagCount; // We might increase the number of push tags to other blocks. for (AssemblyItem const& inlinedItem: inlinableBlock->items) if (inlinedItem.type() == PushTag) if (optional duplicatedTag = getLocalTag(inlinedItem)) if (auto* block = util::valueOrNullptr(inlinableBlocks, *duplicatedTag)) ++block->pushTagCount; // Skip the original jump to the inlined tag and continue. ++it; continue; } } } newItems.emplace_back(item); } m_items = move(newItems); }