Coverage Report

Created: 2025-09-08 08:10

/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 solidity;
42
using namespace solidity::evmasm;
43
44
45
namespace
46
{
47
/// @returns an estimation of the runtime gas cost of the AssemblyItems in @a _itemRange.
48
template<typename RangeType>
49
u256 executionCost(RangeType const& _itemRange, langutil::EVMVersion _evmVersion)
50
66.6k
{
51
66.6k
  GasMeter gasMeter{std::make_shared<KnownState>(), _evmVersion};
52
66.6k
  auto gasConsumption = ranges::accumulate(_itemRange | ranges::views::transform(
53
199k
    [&gasMeter](auto const& _item) { return gasMeter.estimateMax(_item, false); }
54
66.6k
  ), GasMeter::GasConsumption());
55
66.6k
  if (gasConsumption.isInfinite)
56
0
    return std::numeric_limits<u256>::max();
57
66.6k
  else
58
66.6k
    return gasConsumption.value;
59
66.6k
}
60
/// @returns an estimation of the code size in bytes needed for the AssemblyItems in @a _itemRange.
61
template<typename RangeType>
62
uint64_t codeSize(RangeType const& _itemRange, langutil::EVMVersion _evmVersion)
63
360k
{
64
360k
  return ranges::accumulate(_itemRange | ranges::views::transform(
65
2.01M
    [&](auto const& _item) { return _item.bytesRequired(2, _evmVersion, 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&, solidity::langutil::EVMVersion)::{lambda(auto:1 const&)#1}::operator()<solidity::evmasm::AssemblyItem>(solidity::evmasm::AssemblyItem const&) const
Line
Count
Source
65
526k
    [&](auto const& _item) { return _item.bytesRequired(2, _evmVersion, 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&, solidity::langutil::EVMVersion)::{lambda(auto:1 const&)#1}::operator()<solidity::evmasm::AssemblyItem>(solidity::evmasm::AssemblyItem const&) const
Line
Count
Source
65
460k
    [&](auto const& _item) { return _item.bytesRequired(2, _evmVersion, Precision::Approximate); }
Inliner.cpp:auto (anonymous namespace)::codeSize<ranges::span<solidity::evmasm::AssemblyItem const, -1l> >(ranges::span<solidity::evmasm::AssemblyItem const, -1l> const&, solidity::langutil::EVMVersion)::{lambda(auto:1 const&)#1}::operator()<solidity::evmasm::AssemblyItem>(solidity::evmasm::AssemblyItem const&) const
Line
Count
Source
65
1.02M
    [&](auto const& _item) { return _item.bytesRequired(2, _evmVersion, Precision::Approximate); }
66
360k
  ), 0u);
67
360k
}
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&, solidity::langutil::EVMVersion)
Line
Count
Source
63
33.3k
{
64
33.3k
  return ranges::accumulate(_itemRange | ranges::views::transform(
65
33.3k
    [&](auto const& _item) { return _item.bytesRequired(2, _evmVersion, Precision::Approximate); }
66
33.3k
  ), 0u);
67
33.3k
}
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&, solidity::langutil::EVMVersion)
Line
Count
Source
63
197k
{
64
197k
  return ranges::accumulate(_itemRange | ranges::views::transform(
65
197k
    [&](auto const& _item) { return _item.bytesRequired(2, _evmVersion, Precision::Approximate); }
66
197k
  ), 0u);
67
197k
}
Inliner.cpp:unsigned long (anonymous namespace)::codeSize<ranges::span<solidity::evmasm::AssemblyItem const, -1l> >(ranges::span<solidity::evmasm::AssemblyItem const, -1l> const&, solidity::langutil::EVMVersion)
Line
Count
Source
63
130k
{
64
130k
  return ranges::accumulate(_itemRange | ranges::views::transform(
65
130k
    [&](auto const& _item) { return _item.bytesRequired(2, _evmVersion, Precision::Approximate); }
66
130k
  ), 0u);
67
130k
}
68
/// @returns the tag id, if @a _item is a PushTag or Tag into the current subassembly, std::nullopt otherwise.
69
std::optional<size_t> getLocalTag(AssemblyItem const& _item)
70
5.98M
{
71
5.98M
  if (_item.type() != PushTag && _item.type() != Tag)
72
0
    return std::nullopt;
73
5.98M
  auto [subId, tag] = _item.splitForeignPushTag();
74
5.98M
  if (!subId.empty())
75
0
    return std::nullopt;
76
5.98M
  return tag;
77
5.98M
}
78
}
79
80
bool Inliner::isInlineCandidate(size_t _tag, ranges::span<AssemblyItem const> _items) const
81
1.36M
{
82
1.36M
  assertThrow(_items.size() > 0, OptimizerException, "");
83
84
1.36M
  if (_items.back().type() != Operation)
85
142k
    return false;
86
1.22M
  if (
87
1.22M
    _items.back() != Instruction::JUMP &&
88
1.22M
    !SemanticInformation::terminatesControlFlow(_items.back().instruction())
89
1.22M
  )
90
328k
    return false;
91
92
  // Never inline tags that reference themselves.
93
896k
  for (AssemblyItem const& item: _items)
94
7.70M
    if (item.type() == PushTag)
95
918k
      if (getLocalTag(item) == _tag)
96
745
          return false;
97
98
895k
  return true;
99
896k
}
100
101
std::map<size_t, Inliner::InlinableBlock> Inliner::determineInlinableBlocks(AssemblyItems const& _items) const
102
75.1k
{
103
75.1k
  std::map<size_t, ranges::span<AssemblyItem const>> inlinableBlockItems;
104
75.1k
  std::map<size_t, uint64_t> numPushTags;
105
75.1k
  std::optional<size_t> lastTag;
106
75.1k
  for (auto&& [index, item]: _items | ranges::views::enumerate)
107
17.3M
  {
108
    // The number of PushTags approximates the number of calls to a block.
109
17.3M
    if (item.type() == PushTag)
110
1.64M
      if (std::optional<size_t> tag = getLocalTag(item))
111
1.64M
        ++numPushTags[*tag];
112
113
    // We can only inline blocks with straight control flow that end in a jump.
114
    // Using breaksCSEAnalysisBlock will hopefully allow the return jump to be optimized after inlining.
115
17.3M
    if (lastTag && SemanticInformation::breaksCSEAnalysisBlock(item, false))
116
1.36M
    {
117
1.36M
      ranges::span<AssemblyItem const> block = _items | ranges::views::slice(*lastTag + 1, index + 1);
118
1.36M
      if (std::optional<size_t> tag = getLocalTag(_items[*lastTag]))
119
1.36M
        if (isInlineCandidate(*tag, block))
120
895k
          inlinableBlockItems[*tag] = block;
121
1.36M
      lastTag.reset();
122
1.36M
    }
123
124
17.3M
    if (item.type() == Tag)
125
1.37M
    {
126
1.37M
      assertThrow(getLocalTag(item), OptimizerException, "");
127
1.37M
      lastTag = index;
128
1.37M
    }
129
17.3M
  }
130
131
  // Store the number of PushTags alongside the assembly items and discard tags that are never pushed.
132
75.1k
  std::map<size_t, InlinableBlock> result;
133
75.1k
  for (auto&& [tag, items]: inlinableBlockItems)
134
895k
    if (uint64_t const* numPushes = util::valueOrNullptr(numPushTags, tag))
135
747k
      result.emplace(tag, InlinableBlock{items, *numPushes});
136
75.1k
  return result;
137
75.1k
}
138
139
bool Inliner::shouldInlineFullFunctionBody(size_t _tag, ranges::span<AssemblyItem const> _block, uint64_t _pushTagCount) const
140
33.3k
{
141
  // Accumulate size of the inline candidate block in bytes (without the return jump).
142
33.3k
  uint64_t functionBodySize = codeSize(ranges::views::drop_last(_block, 1), m_evmVersion);
143
144
  // Use the number of push tags as approximation of the average number of calls to the function per run.
145
33.3k
  uint64_t numberOfCalls = _pushTagCount;
146
  // Also use the number of push tags as approximation of the number of call sites to the function.
147
33.3k
  uint64_t numberOfCallSites = _pushTagCount;
148
149
33.3k
  static AssemblyItems const uninlinedCallSitePattern = {
150
33.3k
    AssemblyItem{PushTag},
151
33.3k
    AssemblyItem{PushTag},
152
33.3k
    AssemblyItem{Instruction::JUMP},
153
33.3k
    AssemblyItem{Tag}
154
33.3k
  };
155
33.3k
  static AssemblyItems const uninlinedFunctionPattern = {
156
33.3k
    AssemblyItem{Tag},
157
    // Actual function body of size functionBodySize. Handled separately below.
158
33.3k
    AssemblyItem{Instruction::JUMP}
159
33.3k
  };
160
161
  // Both the call site and jump site pattern is executed for each call.
162
  // Since the function body has to be executed equally often both with and without inlining,
163
  // it can be ignored.
164
33.3k
  bigint uninlinedExecutionCost = numberOfCalls * (
165
33.3k
    executionCost(uninlinedCallSitePattern, m_evmVersion) +
166
33.3k
    executionCost(uninlinedFunctionPattern, m_evmVersion)
167
33.3k
  );
168
  // Each call site deposits the call site pattern, whereas the jump site pattern and the function itself are deposited once.
169
33.3k
  bigint uninlinedDepositCost = GasMeter::dataGas(
170
33.3k
    numberOfCallSites * codeSize(uninlinedCallSitePattern, m_evmVersion) +
171
33.3k
    codeSize(uninlinedFunctionPattern, m_evmVersion) +
172
33.3k
    functionBodySize,
173
33.3k
    m_isCreation,
174
33.3k
    m_evmVersion
175
33.3k
  );
176
  // When inlining the execution cost beyond the actual function execution is zero,
177
  // but for each call site a copy of the function is deposited.
178
33.3k
  bigint inlinedDepositCost = GasMeter::dataGas(
179
33.3k
    numberOfCallSites * functionBodySize,
180
33.3k
    m_isCreation,
181
33.3k
    m_evmVersion
182
33.3k
  );
183
  // If the block is referenced from outside the current subassembly, the original function cannot be removed.
184
  // Note that the function also cannot always be removed, if it is not referenced from outside, but in that case
185
  // the heuristics is optimistic.
186
33.3k
  if (m_tagsReferencedFromOutside.count(_tag))
187
0
    inlinedDepositCost += GasMeter::dataGas(
188
0
      codeSize(uninlinedFunctionPattern, m_evmVersion) + functionBodySize,
189
0
      m_isCreation,
190
0
      m_evmVersion
191
0
    );
192
193
  // If the estimated runtime cost over the lifetime of the contract plus the deposit cost in the uninlined case
194
  // exceed the inlined deposit costs, it is beneficial to inline.
195
33.3k
  if (bigint(m_runs) * uninlinedExecutionCost + uninlinedDepositCost > inlinedDepositCost)
196
32.4k
    return true;
197
198
823
  return false;
199
33.3k
}
200
201
std::optional<AssemblyItem> Inliner::shouldInline(size_t _tag, AssemblyItem const& _jump, InlinableBlock const& _block) const
202
282k
{
203
282k
  assertThrow(_jump == Instruction::JUMP, OptimizerException, "");
204
282k
  AssemblyItem blockExit = _block.items.back();
205
206
282k
  if (
207
282k
    _jump.getJumpType() == AssemblyItem::JumpType::IntoFunction &&
208
282k
    blockExit == Instruction::JUMP &&
209
282k
    blockExit.getJumpType() == AssemblyItem::JumpType::OutOfFunction &&
210
282k
    shouldInlineFullFunctionBody(_tag, _block.items, _block.pushTagCount)
211
282k
  )
212
32.4k
  {
213
32.4k
    blockExit.setJumpType(AssemblyItem::JumpType::Ordinary);
214
32.4k
    return blockExit;
215
32.4k
  }
216
217
  // Inline small blocks, if the jump to it is ordinary or the blockExit is a terminating instruction.
218
249k
  if (
219
249k
    _jump.getJumpType() == AssemblyItem::JumpType::Ordinary ||
220
249k
    SemanticInformation::terminatesControlFlow(blockExit.instruction())
221
249k
  )
222
130k
  {
223
130k
    static AssemblyItems const jumpPattern = {
224
130k
      AssemblyItem{PushTag},
225
130k
      AssemblyItem{Instruction::JUMP},
226
130k
    };
227
130k
    if (
228
130k
      GasMeter::dataGas(codeSize(_block.items, m_evmVersion), m_isCreation, m_evmVersion) <=
229
130k
      GasMeter::dataGas(codeSize(jumpPattern, m_evmVersion), m_isCreation, m_evmVersion)
230
130k
    )
231
20.1k
      return blockExit;
232
130k
  }
233
234
229k
  return std::nullopt;
235
249k
}
236
237
238
void Inliner::optimise()
239
75.1k
{
240
75.1k
  std::map<size_t, InlinableBlock> inlinableBlocks = determineInlinableBlocks(m_items);
241
242
75.1k
  if (inlinableBlocks.empty())
243
18.9k
    return;
244
245
56.2k
  AssemblyItems newItems;
246
16.1M
  for (auto it = m_items.begin(); it != m_items.end(); ++it)
247
16.0M
  {
248
16.0M
    AssemblyItem const& item = *it;
249
16.0M
    if (next(it) != m_items.end())
250
16.0M
    {
251
16.0M
      AssemblyItem const& nextItem = *next(it);
252
16.0M
      if (item.type() == PushTag && nextItem == Instruction::JUMP)
253
676k
      {
254
676k
        if (std::optional<size_t> tag = getLocalTag(item))
255
676k
          if (auto* inlinableBlock = util::valueOrNullptr(inlinableBlocks, *tag))
256
282k
            if (auto exitItem = shouldInline(*tag, nextItem, *inlinableBlock))
257
52.6k
            {
258
52.6k
              newItems += inlinableBlock->items | ranges::views::drop_last(1);
259
52.6k
              newItems.emplace_back(std::move(*exitItem));
260
261
              // We are removing one push tag to the block we inline.
262
52.6k
              --inlinableBlock->pushTagCount;
263
              // We might increase the number of push tags to other blocks.
264
52.6k
              for (AssemblyItem const& inlinedItem: inlinableBlock->items)
265
533k
                if (inlinedItem.type() == PushTag)
266
3.67k
                  if (std::optional<size_t> duplicatedTag = getLocalTag(inlinedItem))
267
3.67k
                    if (auto* block = util::valueOrNullptr(inlinableBlocks, *duplicatedTag))
268
2.66k
                      ++block->pushTagCount;
269
270
              // Skip the original jump to the inlined tag and continue.
271
52.6k
              ++it;
272
52.6k
              continue;
273
52.6k
            }
274
676k
      }
275
16.0M
    }
276
16.0M
    newItems.emplace_back(item);
277
16.0M
  }
278
279
56.2k
  m_items = std::move(newItems);
280
56.2k
}