Coverage Report

Created: 2022-08-24 06:31

/src/solidity/libevmasm/Inliner.cpp
Line
Count
Source (jump to first uncovered line)
1
/*
2
  This file is part of solidity.
3
4
  solidity is free software: you can redistribute it and/or modify
5
  it under the terms of the GNU General Public License as published by
6
  the Free Software Foundation, either version 3 of the License, or
7
  (at your option) any later version.
8
9
  solidity is distributed in the hope that it will be useful,
10
  but WITHOUT ANY WARRANTY; without even the implied warranty of
11
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
  GNU General Public License for more details.
13
14
  You should have received a copy of the GNU General Public License
15
  along with solidity.  If not, see <http://www.gnu.org/licenses/>.
16
*/
17
// SPDX-License-Identifier: GPL-3.0
18
/**
19
 * @file Inliner.cpp
20
 * Inlines small code snippets by replacing JUMP with a copy of the code jumped to.
21
 */
22
23
#include <libevmasm/Inliner.h>
24
25
#include <libevmasm/AssemblyItem.h>
26
#include <libevmasm/GasMeter.h>
27
#include <libevmasm/KnownState.h>
28
#include <libevmasm/SemanticInformation.h>
29
30
#include <libsolutil/CommonData.h>
31
32
#include <range/v3/numeric/accumulate.hpp>
33
#include <range/v3/view/drop_last.hpp>
34
#include <range/v3/view/enumerate.hpp>
35
#include <range/v3/view/slice.hpp>
36
#include <range/v3/view/transform.hpp>
37
38
#include <optional>
39
#include <limits>
40
41
using namespace std;
42
using namespace solidity;
43
using namespace solidity::evmasm;
44
45
46
namespace
47
{
48
/// @returns an estimation of the runtime gas cost of the AsssemblyItems in @a _itemRange.
49
template<typename RangeType>
50
u256 executionCost(RangeType const& _itemRange, langutil::EVMVersion _evmVersion)
51
0
{
52
0
  GasMeter gasMeter{std::make_shared<KnownState>(), _evmVersion};
53
0
  auto gasConsumption = ranges::accumulate(_itemRange | ranges::views::transform(
54
0
    [&gasMeter](auto const& _item) { return gasMeter.estimateMax(_item, false); }
55
0
  ), GasMeter::GasConsumption());
56
0
  if (gasConsumption.isInfinite)
57
0
    return numeric_limits<u256>::max();
58
0
  else
59
0
    return gasConsumption.value;
60
0
}
61
/// @returns an estimation of the code size in bytes needed for the AssemblyItems in @a _itemRange.
62
template<typename RangeType>
63
uint64_t codeSize(RangeType const& _itemRange)
64
0
{
65
0
  return ranges::accumulate(_itemRange | ranges::views::transform(
66
0
    [](auto const& _item) { return _item.bytesRequired(2, Precision::Approximate); }
Unexecuted instantiation: Inliner.cpp:auto (anonymous namespace)::codeSize<ranges::drop_last_view<ranges::span<solidity::evmasm::AssemblyItem const, -1l>, std::__1::integral_constant<ranges::detail::drop_last_view::mode_enum, (ranges::detail::drop_last_view::mode_enum)0> > >(ranges::drop_last_view<ranges::span<solidity::evmasm::AssemblyItem const, -1l>, std::__1::integral_constant<ranges::detail::drop_last_view::mode_enum, (ranges::detail::drop_last_view::mode_enum)0> > const&)::{lambda(auto:1 const&)#1}::operator()<solidity::evmasm::AssemblyItem>(solidity::evmasm::AssemblyItem const&) const
Unexecuted instantiation: Inliner.cpp:auto (anonymous namespace)::codeSize<std::__1::vector<solidity::evmasm::AssemblyItem, std::__1::allocator<solidity::evmasm::AssemblyItem> > >(std::__1::vector<solidity::evmasm::AssemblyItem, std::__1::allocator<solidity::evmasm::AssemblyItem> > const&)::{lambda(auto:1 const&)#1}::operator()<solidity::evmasm::AssemblyItem>(solidity::evmasm::AssemblyItem const&) const
Unexecuted instantiation: Inliner.cpp:auto (anonymous namespace)::codeSize<ranges::span<solidity::evmasm::AssemblyItem const, -1l> >(ranges::span<solidity::evmasm::AssemblyItem const, -1l> const&)::{lambda(auto:1 const&)#1}::operator()<solidity::evmasm::AssemblyItem>(solidity::evmasm::AssemblyItem const&) const
67
0
  ), 0u);
68
0
}
Unexecuted instantiation: Inliner.cpp:unsigned long (anonymous namespace)::codeSize<ranges::drop_last_view<ranges::span<solidity::evmasm::AssemblyItem const, -1l>, std::__1::integral_constant<ranges::detail::drop_last_view::mode_enum, (ranges::detail::drop_last_view::mode_enum)0> > >(ranges::drop_last_view<ranges::span<solidity::evmasm::AssemblyItem const, -1l>, std::__1::integral_constant<ranges::detail::drop_last_view::mode_enum, (ranges::detail::drop_last_view::mode_enum)0> > const&)
Unexecuted instantiation: Inliner.cpp:unsigned long (anonymous namespace)::codeSize<std::__1::vector<solidity::evmasm::AssemblyItem, std::__1::allocator<solidity::evmasm::AssemblyItem> > >(std::__1::vector<solidity::evmasm::AssemblyItem, std::__1::allocator<solidity::evmasm::AssemblyItem> > const&)
Unexecuted instantiation: Inliner.cpp:unsigned long (anonymous namespace)::codeSize<ranges::span<solidity::evmasm::AssemblyItem const, -1l> >(ranges::span<solidity::evmasm::AssemblyItem const, -1l> const&)
69
/// @returns the tag id, if @a _item is a PushTag or Tag into the current subassembly, nullopt otherwise.
70
optional<size_t> getLocalTag(AssemblyItem const& _item)
71
0
{
72
0
  if (_item.type() != PushTag && _item.type() != Tag)
73
0
    return nullopt;
74
0
  auto [subId, tag] = _item.splitForeignPushTag();
75
0
  if (subId != numeric_limits<size_t>::max())
76
0
    return nullopt;
77
0
  return tag;
78
0
}
79
}
80
81
bool Inliner::isInlineCandidate(size_t _tag, ranges::span<AssemblyItem const> _items) const
82
0
{
83
0
  assertThrow(_items.size() > 0, OptimizerException, "");
84
85
0
  if (_items.back().type() != Operation)
86
0
    return false;
87
0
  if (
88
0
    _items.back() != Instruction::JUMP &&
89
0
    !SemanticInformation::terminatesControlFlow(_items.back().instruction())
90
0
  )
91
0
    return false;
92
93
  // Never inline tags that reference themselves.
94
0
  for (AssemblyItem const& item: _items)
95
0
    if (item.type() == PushTag)
96
0
      if (getLocalTag(item) == _tag)
97
0
          return false;
98
99
0
  return true;
100
0
}
101
102
map<size_t, Inliner::InlinableBlock> Inliner::determineInlinableBlocks(AssemblyItems const& _items) const
103
0
{
104
0
  std::map<size_t, ranges::span<AssemblyItem const>> inlinableBlockItems;
105
0
  std::map<size_t, uint64_t> numPushTags;
106
0
  std::optional<size_t> lastTag;
107
0
  for (auto&& [index, item]: _items | ranges::views::enumerate)
108
0
  {
109
    // The number of PushTags approximates the number of calls to a block.
110
0
    if (item.type() == PushTag)
111
0
      if (optional<size_t> tag = getLocalTag(item))
112
0
        ++numPushTags[*tag];
113
114
    // We can only inline blocks with straight control flow that end in a jump.
115
    // Using breaksCSEAnalysisBlock will hopefully allow the return jump to be optimized after inlining.
116
0
    if (lastTag && SemanticInformation::breaksCSEAnalysisBlock(item, false))
117
0
    {
118
0
      ranges::span<AssemblyItem const> block = _items | ranges::views::slice(*lastTag + 1, index + 1);
119
0
      if (optional<size_t> tag = getLocalTag(_items[*lastTag]))
120
0
        if (isInlineCandidate(*tag, block))
121
0
          inlinableBlockItems[*tag] = block;
122
0
      lastTag.reset();
123
0
    }
124
125
0
    if (item.type() == Tag)
126
0
    {
127
0
      assertThrow(getLocalTag(item), OptimizerException, "");
128
0
      lastTag = index;
129
0
    }
130
0
  }
131
132
  // Store the number of PushTags alongside the assembly items and discard tags that are never pushed.
133
0
  map<size_t, InlinableBlock> result;
134
0
  for (auto&& [tag, items]: inlinableBlockItems)
135
0
    if (uint64_t const* numPushes = util::valueOrNullptr(numPushTags, tag))
136
0
      result.emplace(tag, InlinableBlock{items, *numPushes});
137
0
  return result;
138
0
}
139
140
bool Inliner::shouldInlineFullFunctionBody(size_t _tag, ranges::span<AssemblyItem const> _block, uint64_t _pushTagCount) const
141
0
{
142
  // Accumulate size of the inline candidate block in bytes (without the return jump).
143
0
  uint64_t functionBodySize = codeSize(ranges::views::drop_last(_block, 1));
144
145
  // Use the number of push tags as approximation of the average number of calls to the function per run.
146
0
  uint64_t numberOfCalls = _pushTagCount;
147
  // Also use the number of push tags as approximation of the number of call sites to the function.
148
0
  uint64_t numberOfCallSites = _pushTagCount;
149
150
0
  static AssemblyItems const uninlinedCallSitePattern = {
151
0
    AssemblyItem{PushTag},
152
0
    AssemblyItem{PushTag},
153
0
    AssemblyItem{Instruction::JUMP},
154
0
    AssemblyItem{Tag}
155
0
  };
156
0
  static AssemblyItems const uninlinedFunctionPattern = {
157
0
    AssemblyItem{Tag},
158
    // Actual function body of size functionBodySize. Handled separately below.
159
0
    AssemblyItem{Instruction::JUMP}
160
0
  };
161
162
  // Both the call site and jump site pattern is executed for each call.
163
  // Since the function body has to be executed equally often both with and without inlining,
164
  // it can be ignored.
165
0
  bigint uninlinedExecutionCost = numberOfCalls * (
166
0
    executionCost(uninlinedCallSitePattern, m_evmVersion) +
167
0
    executionCost(uninlinedFunctionPattern, m_evmVersion)
168
0
  );
169
  // Each call site deposits the call site pattern, whereas the jump site pattern and the function itself are deposited once.
170
0
  bigint uninlinedDepositCost = GasMeter::dataGas(
171
0
    numberOfCallSites * codeSize(uninlinedCallSitePattern) +
172
0
    codeSize(uninlinedFunctionPattern) +
173
0
    functionBodySize,
174
0
    m_isCreation,
175
0
    m_evmVersion
176
0
  );
177
  // When inlining the execution cost beyond the actual function execution is zero,
178
  // but for each call site a copy of the function is deposited.
179
0
  bigint inlinedDepositCost = GasMeter::dataGas(
180
0
    numberOfCallSites * functionBodySize,
181
0
    m_isCreation,
182
0
    m_evmVersion
183
0
  );
184
  // If the block is referenced from outside the current subassembly, the original function cannot be removed.
185
  // Note that the function also cannot always be removed, if it is not referenced from outside, but in that case
186
  // the heuristics is optimistic.
187
0
  if (m_tagsReferencedFromOutside.count(_tag))
188
0
    inlinedDepositCost += GasMeter::dataGas(
189
0
      codeSize(uninlinedFunctionPattern) + functionBodySize,
190
0
      m_isCreation,
191
0
      m_evmVersion
192
0
    );
193
194
  // If the estimated runtime cost over the lifetime of the contract plus the deposit cost in the uninlined case
195
  // exceed the inlined deposit costs, it is beneficial to inline.
196
0
  if (bigint(m_runs) * uninlinedExecutionCost + uninlinedDepositCost > inlinedDepositCost)
197
0
    return true;
198
199
0
  return false;
200
0
}
201
202
optional<AssemblyItem> Inliner::shouldInline(size_t _tag, AssemblyItem const& _jump, InlinableBlock const& _block) const
203
0
{
204
0
  assertThrow(_jump == Instruction::JUMP, OptimizerException, "");
205
0
  AssemblyItem blockExit = _block.items.back();
206
207
0
  if (
208
0
    _jump.getJumpType() == AssemblyItem::JumpType::IntoFunction &&
209
0
    blockExit == Instruction::JUMP &&
210
0
    blockExit.getJumpType() == AssemblyItem::JumpType::OutOfFunction &&
211
0
    shouldInlineFullFunctionBody(_tag, _block.items, _block.pushTagCount)
212
0
  )
213
0
  {
214
0
    blockExit.setJumpType(AssemblyItem::JumpType::Ordinary);
215
0
    return blockExit;
216
0
  }
217
218
  // Inline small blocks, if the jump to it is ordinary or the blockExit is a terminating instruction.
219
0
  if (
220
0
    _jump.getJumpType() == AssemblyItem::JumpType::Ordinary ||
221
0
    SemanticInformation::terminatesControlFlow(blockExit.instruction())
222
0
  )
223
0
  {
224
0
    static AssemblyItems const jumpPattern = {
225
0
      AssemblyItem{PushTag},
226
0
      AssemblyItem{Instruction::JUMP},
227
0
    };
228
0
    if (
229
0
      GasMeter::dataGas(codeSize(_block.items), m_isCreation, m_evmVersion) <=
230
0
      GasMeter::dataGas(codeSize(jumpPattern), m_isCreation, m_evmVersion)
231
0
    )
232
0
      return blockExit;
233
0
  }
234
235
0
  return nullopt;
236
0
}
237
238
239
void Inliner::optimise()
240
0
{
241
0
  std::map<size_t, InlinableBlock> inlinableBlocks = determineInlinableBlocks(m_items);
242
243
0
  if (inlinableBlocks.empty())
244
0
    return;
245
246
0
  AssemblyItems newItems;
247
0
  for (auto it = m_items.begin(); it != m_items.end(); ++it)
248
0
  {
249
0
    AssemblyItem const& item = *it;
250
0
    if (next(it) != m_items.end())
251
0
    {
252
0
      AssemblyItem const& nextItem = *next(it);
253
0
      if (item.type() == PushTag && nextItem == Instruction::JUMP)
254
0
      {
255
0
        if (optional<size_t> tag = getLocalTag(item))
256
0
          if (auto* inlinableBlock = util::valueOrNullptr(inlinableBlocks, *tag))
257
0
            if (auto exitItem = shouldInline(*tag, nextItem, *inlinableBlock))
258
0
            {
259
0
              newItems += inlinableBlock->items | ranges::views::drop_last(1);
260
0
              newItems.emplace_back(move(*exitItem));
261
262
              // We are removing one push tag to the block we inline.
263
0
              --inlinableBlock->pushTagCount;
264
              // We might increase the number of push tags to other blocks.
265
0
              for (AssemblyItem const& inlinedItem: inlinableBlock->items)
266
0
                if (inlinedItem.type() == PushTag)
267
0
                  if (optional<size_t> duplicatedTag = getLocalTag(inlinedItem))
268
0
                    if (auto* block = util::valueOrNullptr(inlinableBlocks, *duplicatedTag))
269
0
                      ++block->pushTagCount;
270
271
              // Skip the original jump to the inlined tag and continue.
272
0
              ++it;
273
0
              continue;
274
0
            }
275
0
      }
276
0
    }
277
0
    newItems.emplace_back(item);
278
0
  }
279
280
0
  m_items = move(newItems);
281
0
}