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