Coverage Report

Created: 2022-08-24 06:32

/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
62.4k
{
52
62.4k
  GasMeter gasMeter{std::make_shared<KnownState>(), _evmVersion};
53
62.4k
  auto gasConsumption = ranges::accumulate(_itemRange | ranges::views::transform(
54
187k
    [&gasMeter](auto const& _item) { return gasMeter.estimateMax(_item, false); }
55
62.4k
  ), GasMeter::GasConsumption());
56
62.4k
  if (gasConsumption.isInfinite)
57
0
    return numeric_limits<u256>::max();
58
62.4k
  else
59
62.4k
    return gasConsumption.value;
60
62.4k
}
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
235k
{
65
235k
  return ranges::accumulate(_itemRange | ranges::views::transform(
66
1.65M
    [](auto const& _item) { return _item.bytesRequired(2, Precision::Approximate); }
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
Line
Count
Source
66
594k
    [](auto const& _item) { return _item.bytesRequired(2, Precision::Approximate); }
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
Line
Count
Source
66
329k
    [](auto const& _item) { return _item.bytesRequired(2, Precision::Approximate); }
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
Line
Count
Source
66
728k
    [](auto const& _item) { return _item.bytesRequired(2, Precision::Approximate); }
67
235k
  ), 0u);
68
235k
}
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&)
Line
Count
Source
64
31.2k
{
65
31.2k
  return ranges::accumulate(_itemRange | ranges::views::transform(
66
31.2k
    [](auto const& _item) { return _item.bytesRequired(2, Precision::Approximate); }
67
31.2k
  ), 0u);
68
31.2k
}
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&)
Line
Count
Source
64
133k
{
65
133k
  return ranges::accumulate(_itemRange | ranges::views::transform(
66
133k
    [](auto const& _item) { return _item.bytesRequired(2, Precision::Approximate); }
67
133k
  ), 0u);
68
133k
}
Inliner.cpp:unsigned long (anonymous namespace)::codeSize<ranges::span<solidity::evmasm::AssemblyItem const, -1l> >(ranges::span<solidity::evmasm::AssemblyItem const, -1l> const&)
Line
Count
Source
64
70.9k
{
65
70.9k
  return ranges::accumulate(_itemRange | ranges::views::transform(
66
70.9k
    [](auto const& _item) { return _item.bytesRequired(2, Precision::Approximate); }
67
70.9k
  ), 0u);
68
70.9k
}
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
3.54M
{
72
3.54M
  if (_item.type() != PushTag && _item.type() != Tag)
73
0
    return nullopt;
74
3.54M
  auto [subId, tag] = _item.splitForeignPushTag();
75
3.54M
  if (subId != numeric_limits<size_t>::max())
76
0
    return nullopt;
77
3.54M
  return tag;
78
3.54M
}
79
}
80
81
bool Inliner::isInlineCandidate(size_t _tag, ranges::span<AssemblyItem const> _items) const
82
861k
{
83
861k
  assertThrow(_items.size() > 0, OptimizerException, "");
84
85
861k
  if (_items.back().type() != Operation)
86
23.7k
    return false;
87
837k
  if (
88
837k
    _items.back() != Instruction::JUMP &&
89
837k
    !SemanticInformation::terminatesControlFlow(_items.back().instruction())
90
837k
  )
91
210k
    return false;
92
93
  // Never inline tags that reference themselves.
94
626k
  for (AssemblyItem const& item: _items)
95
4.79M
    if (item.type() == PushTag)
96
628k
      if (getLocalTag(item) == _tag)
97
1.65k
          return false;
98
99
625k
  return true;
100
626k
}
101
102
map<size_t, Inliner::InlinableBlock> Inliner::determineInlinableBlocks(AssemblyItems const& _items) const
103
71.1k
{
104
71.1k
  std::map<size_t, ranges::span<AssemblyItem const>> inlinableBlockItems;
105
71.1k
  std::map<size_t, uint64_t> numPushTags;
106
71.1k
  std::optional<size_t> lastTag;
107
71.1k
  for (auto&& [index, item]: _items | ranges::views::enumerate)
108
8.59M
  {
109
    // The number of PushTags approximates the number of calls to a block.
110
8.59M
    if (item.type() == PushTag)
111
878k
      if (optional<size_t> tag = getLocalTag(item))
112
878k
        ++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
8.59M
    if (lastTag && SemanticInformation::breaksCSEAnalysisBlock(item, false))
117
861k
    {
118
861k
      ranges::span<AssemblyItem const> block = _items | ranges::views::slice(*lastTag + 1, index + 1);
119
861k
      if (optional<size_t> tag = getLocalTag(_items[*lastTag]))
120
861k
        if (isInlineCandidate(*tag, block))
121
625k
          inlinableBlockItems[*tag] = block;
122
861k
      lastTag.reset();
123
861k
    }
124
125
8.59M
    if (item.type() == Tag)
126
861k
    {
127
861k
      assertThrow(getLocalTag(item), OptimizerException, "");
128
861k
      lastTag = index;
129
861k
    }
130
8.59M
  }
131
132
  // Store the number of PushTags alongside the assembly items and discard tags that are never pushed.
133
71.1k
  map<size_t, InlinableBlock> result;
134
71.1k
  for (auto&& [tag, items]: inlinableBlockItems)
135
625k
    if (uint64_t const* numPushes = util::valueOrNullptr(numPushTags, tag))
136
592k
      result.emplace(tag, InlinableBlock{items, *numPushes});
137
71.1k
  return result;
138
71.1k
}
139
140
bool Inliner::shouldInlineFullFunctionBody(size_t _tag, ranges::span<AssemblyItem const> _block, uint64_t _pushTagCount) const
141
31.2k
{
142
  // Accumulate size of the inline candidate block in bytes (without the return jump).
143
31.2k
  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
31.2k
  uint64_t numberOfCalls = _pushTagCount;
147
  // Also use the number of push tags as approximation of the number of call sites to the function.
148
31.2k
  uint64_t numberOfCallSites = _pushTagCount;
149
150
31.2k
  static AssemblyItems const uninlinedCallSitePattern = {
151
31.2k
    AssemblyItem{PushTag},
152
31.2k
    AssemblyItem{PushTag},
153
31.2k
    AssemblyItem{Instruction::JUMP},
154
31.2k
    AssemblyItem{Tag}
155
31.2k
  };
156
31.2k
  static AssemblyItems const uninlinedFunctionPattern = {
157
31.2k
    AssemblyItem{Tag},
158
    // Actual function body of size functionBodySize. Handled separately below.
159
31.2k
    AssemblyItem{Instruction::JUMP}
160
31.2k
  };
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
31.2k
  bigint uninlinedExecutionCost = numberOfCalls * (
166
31.2k
    executionCost(uninlinedCallSitePattern, m_evmVersion) +
167
31.2k
    executionCost(uninlinedFunctionPattern, m_evmVersion)
168
31.2k
  );
169
  // Each call site deposits the call site pattern, whereas the jump site pattern and the function itself are deposited once.
170
31.2k
  bigint uninlinedDepositCost = GasMeter::dataGas(
171
31.2k
    numberOfCallSites * codeSize(uninlinedCallSitePattern) +
172
31.2k
    codeSize(uninlinedFunctionPattern) +
173
31.2k
    functionBodySize,
174
31.2k
    m_isCreation,
175
31.2k
    m_evmVersion
176
31.2k
  );
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
31.2k
  bigint inlinedDepositCost = GasMeter::dataGas(
180
31.2k
    numberOfCallSites * functionBodySize,
181
31.2k
    m_isCreation,
182
31.2k
    m_evmVersion
183
31.2k
  );
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
31.2k
  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
31.2k
  if (bigint(m_runs) * uninlinedExecutionCost + uninlinedDepositCost > inlinedDepositCost)
197
30.9k
    return true;
198
199
215
  return false;
200
31.2k
}
201
202
optional<AssemblyItem> Inliner::shouldInline(size_t _tag, AssemblyItem const& _jump, InlinableBlock const& _block) const
203
104k
{
204
104k
  assertThrow(_jump == Instruction::JUMP, OptimizerException, "");
205
104k
  AssemblyItem blockExit = _block.items.back();
206
207
104k
  if (
208
104k
    _jump.getJumpType() == AssemblyItem::JumpType::IntoFunction &&
209
104k
    blockExit == Instruction::JUMP &&
210
104k
    blockExit.getJumpType() == AssemblyItem::JumpType::OutOfFunction &&
211
104k
    shouldInlineFullFunctionBody(_tag, _block.items, _block.pushTagCount)
212
104k
  )
213
30.9k
  {
214
30.9k
    blockExit.setJumpType(AssemblyItem::JumpType::Ordinary);
215
30.9k
    return blockExit;
216
30.9k
  }
217
218
  // Inline small blocks, if the jump to it is ordinary or the blockExit is a terminating instruction.
219
73.8k
  if (
220
73.8k
    _jump.getJumpType() == AssemblyItem::JumpType::Ordinary ||
221
73.8k
    SemanticInformation::terminatesControlFlow(blockExit.instruction())
222
73.8k
  )
223
70.9k
  {
224
70.9k
    static AssemblyItems const jumpPattern = {
225
70.9k
      AssemblyItem{PushTag},
226
70.9k
      AssemblyItem{Instruction::JUMP},
227
70.9k
    };
228
70.9k
    if (
229
70.9k
      GasMeter::dataGas(codeSize(_block.items), m_isCreation, m_evmVersion) <=
230
70.9k
      GasMeter::dataGas(codeSize(jumpPattern), m_isCreation, m_evmVersion)
231
70.9k
    )
232
25.9k
      return blockExit;
233
70.9k
  }
234
235
47.9k
  return nullopt;
236
73.8k
}
237
238
239
void Inliner::optimise()
240
71.1k
{
241
71.1k
  std::map<size_t, InlinableBlock> inlinableBlocks = determineInlinableBlocks(m_items);
242
243
71.1k
  if (inlinableBlocks.empty())
244
23.0k
    return;
245
246
48.0k
  AssemblyItems newItems;
247
8.01M
  for (auto it = m_items.begin(); it != m_items.end(); ++it)
248
7.96M
  {
249
7.96M
    AssemblyItem const& item = *it;
250
7.96M
    if (next(it) != m_items.end())
251
7.92M
    {
252
7.92M
      AssemblyItem const& nextItem = *next(it);
253
7.92M
      if (item.type() == PushTag && nextItem == Instruction::JUMP)
254
306k
      {
255
306k
        if (optional<size_t> tag = getLocalTag(item))
256
306k
          if (auto* inlinableBlock = util::valueOrNullptr(inlinableBlocks, *tag))
257
104k
            if (auto exitItem = shouldInline(*tag, nextItem, *inlinableBlock))
258
56.9k
            {
259
56.9k
              newItems += inlinableBlock->items | ranges::views::drop_last(1);
260
56.9k
              newItems.emplace_back(move(*exitItem));
261
262
              // We are removing one push tag to the block we inline.
263
56.9k
              --inlinableBlock->pushTagCount;
264
              // We might increase the number of push tags to other blocks.
265
56.9k
              for (AssemblyItem const& inlinedItem: inlinableBlock->items)
266
663k
                if (inlinedItem.type() == PushTag)
267
13.9k
                  if (optional<size_t> duplicatedTag = getLocalTag(inlinedItem))
268
13.9k
                    if (auto* block = util::valueOrNullptr(inlinableBlocks, *duplicatedTag))
269
8.71k
                      ++block->pushTagCount;
270
271
              // Skip the original jump to the inlined tag and continue.
272
56.9k
              ++it;
273
56.9k
              continue;
274
56.9k
            }
275
306k
      }
276
7.92M
    }
277
7.90M
    newItems.emplace_back(item);
278
7.90M
  }
279
280
48.0k
  m_items = move(newItems);
281
48.0k
}