size_t MultiMethodInliner::inline_inlinables()

in service/method-inliner/Inliner.cpp [519:822]


size_t MultiMethodInliner::inline_inlinables(
    DexMethod* caller_method,
    const std::vector<Inlinable>& inlinables,
    std::vector<IRInstruction*>* deleted_insns) {
  auto timer = m_inline_inlinables_timer.scope();
  if (for_speed() && m_ab_experiment_context->use_control()) {
    return 0;
  }

  auto caller = caller_method->get_code();
  std::unordered_set<IRCode*> need_deconstruct;
  if (inline_inlinables_need_deconstruct(caller_method)) {
    need_deconstruct.reserve(1 + inlinables.size());
    need_deconstruct.insert(caller);
    for (const auto& inlinable : inlinables) {
      need_deconstruct.insert(inlinable.callee->get_code());
    }
    for (auto code : need_deconstruct) {
      always_assert(!code->editable_cfg_built());
      code->build_cfg(/* editable */ true);
      // if (deleted_insns != nullptr) {
      //   code->cfg().set_removed_insn_ownerhsip(false);
      // }
    }
  }

  // attempt to inline all inlinable candidates
  size_t estimated_caller_size = caller->editable_cfg_built()
                                     ? caller->cfg().sum_opcode_sizes()
                                     : caller->sum_opcode_sizes();

  // Prefer inlining smaller methods first, so that we are less likely to hit
  // overall size limit.
  std::vector<Inlinable> ordered_inlinables(inlinables.begin(),
                                            inlinables.end());

  std::stable_sort(ordered_inlinables.begin(),
                   ordered_inlinables.end(),
                   [&](const Inlinable& a, const Inlinable& b) {
                     // First, prefer no-return inlinable, as they cut off
                     // control-flow and thus other inlinables.
                     if (a.no_return != b.no_return) {
                       return a.no_return > b.no_return;
                     }
                     // Second, prefer smaller methods, to avoid hitting size
                     // limits too soon
                     return a.insn_size < b.insn_size;
                   });

  std::vector<DexMethod*> inlined_callees;
  boost::optional<reg_t> cfg_next_caller_reg;
  if (!m_config.unique_inlined_registers) {
    cfg_next_caller_reg = caller->cfg().get_registers_size();
  }
  size_t calls_not_inlinable{0}, calls_not_inlined{0}, no_returns{0},
      unreachable_insns{0}, caller_too_large{0};

  size_t intermediate_shrinkings{0};
  size_t intermediate_remove_unreachable_blocks{0};
  // We only try intermediate remove-unreachable-blocks or shrinking when using
  // the cfg-inliner, as it will invalidate irlist iterators, which are used
  // with the legacy non-cfg-inliner.
  size_t last_intermediate_inlined_callees{0};

  // Once blocks might have been freed, which can happen via
  // remove_unreachable_blocks and shrinking, callsite pointers are no longer
  // valid.
  std::unique_ptr<std::unordered_set<const IRInstruction*>> remaining_callsites;
  auto recompute_remaining_callsites = [caller, &remaining_callsites,
                                        &ordered_inlinables]() {
    if (!remaining_callsites) {
      remaining_callsites =
          std::make_unique<std::unordered_set<const IRInstruction*>>();
      for (const auto& inlinable : ordered_inlinables) {
        remaining_callsites->insert(inlinable.insn);
      }
    }
    std::unordered_set<const IRInstruction*> new_remaining_callsites;
    for (auto& mie : InstructionIterable(caller->cfg())) {
      if (mie.insn->has_method() && remaining_callsites->count(mie.insn)) {
        new_remaining_callsites.insert(mie.insn);
      }
    }
    always_assert(new_remaining_callsites.size() <=
                  remaining_callsites->size());
    *remaining_callsites = std::move(new_remaining_callsites);
  };

  VisibilityChanges visibility_changes;
  std::unordered_set<DexMethod*> visibility_changes_for;
  size_t init_classes = 0;
  for (const auto& inlinable : ordered_inlinables) {
    auto callee_method = inlinable.callee;
    auto callee = callee_method->get_code();
    auto callsite_insn = inlinable.insn;

    if (remaining_callsites && !remaining_callsites->count(callsite_insn)) {
      if (!inlinable.no_return) {
        calls_not_inlined++;
      }
      continue;
    }

    if (inlinable.no_return) {
      if (!m_config.throw_after_no_return) {
        continue;
      }
      // we are not actually inlining, but just cutting off control-flow
      // afterwards, inserting an (unreachable) "throw null" instruction
      // sequence.
      auto& caller_cfg = caller->cfg();
      auto callsite_it = caller_cfg.find_insn(callsite_insn);
      if (!callsite_it.is_end()) {
        if (m_config.unique_inlined_registers) {
          cfg_next_caller_reg = caller_cfg.get_registers_size();
        }
        auto temp_reg = *cfg_next_caller_reg;
        if (temp_reg >= caller_cfg.get_registers_size()) {
          caller_cfg.set_registers_size(temp_reg + 1);
        }
        // Copying to avoid cfg limitation
        auto* callsite_copy = new IRInstruction(*callsite_it->insn);
        auto* const_insn = (new IRInstruction(OPCODE_CONST))
                               ->set_dest(temp_reg)
                               ->set_literal(0);
        auto* throw_insn =
            (new IRInstruction(OPCODE_THROW))->set_src(0, temp_reg);
        caller_cfg.replace_insns(callsite_it,
                                 {callsite_copy, const_insn, throw_insn});
        auto p = caller_cfg.remove_unreachable_blocks();
        auto unreachable_insn_count = p.first;
        auto registers_size_possibly_reduced = p.second;
        if (registers_size_possibly_reduced &&
            m_config.unique_inlined_registers) {
          caller_cfg.recompute_registers_size();
          cfg_next_caller_reg = caller_cfg.get_registers_size();
        }
        if (unreachable_insn_count) {
          unreachable_insns += unreachable_insn_count;
          recompute_remaining_callsites();
        }
        estimated_caller_size = caller_cfg.sum_opcode_sizes();
        no_returns++;
      }
      continue;
    }

    if (for_speed()) {
      // This is expensive, but with shrinking/non-cfg inlining prep there's no
      // better way. Needs an explicit check to see whether the instruction has
      // already been shrunk away.
      auto callsite_it = caller->cfg().find_insn(callsite_insn);
      if (!callsite_it.is_end()) {
        auto* block = callsite_it.block();
        if (!m_inline_for_speed->should_inline_callsite(caller_method,
                                                        callee_method, block)) {
          calls_not_inlinable++;
          continue;
        }
      }
    }

    bool caller_too_large_;
    auto not_inlinable = !is_inlinable(caller_method, callee_method,
                                       callsite_insn, estimated_caller_size,
                                       inlinable.insn_size, &caller_too_large_);
    if (not_inlinable && caller_too_large_ &&
        inlined_callees.size() > last_intermediate_inlined_callees) {
      intermediate_remove_unreachable_blocks++;
      last_intermediate_inlined_callees = inlined_callees.size();
      auto p = caller->cfg().remove_unreachable_blocks();
      unreachable_insns += p.first;
      auto registers_size_possibly_reduced = p.second;
      if (registers_size_possibly_reduced &&
          m_config.unique_inlined_registers) {
        caller->cfg().recompute_registers_size();
        cfg_next_caller_reg = caller->cfg().get_registers_size();
      }
      estimated_caller_size = caller->cfg().sum_opcode_sizes();
      recompute_remaining_callsites();
      if (!remaining_callsites->count(callsite_insn)) {
        calls_not_inlined++;
        continue;
      }
      not_inlinable = !is_inlinable(caller_method, callee_method, callsite_insn,
                                    estimated_caller_size, inlinable.insn_size,
                                    &caller_too_large_);
      if (!not_inlinable && m_config.intermediate_shrinking &&
          m_shrinker.enabled()) {
        intermediate_shrinkings++;
        m_shrinker.shrink_method(caller_method);
        cfg_next_caller_reg = caller->cfg().get_registers_size();
        estimated_caller_size = caller->cfg().sum_opcode_sizes();
        recompute_remaining_callsites();
        if (!remaining_callsites->count(callsite_insn)) {
          calls_not_inlined++;
          continue;
        }
        not_inlinable = !is_inlinable(caller_method, callee_method,
                                      callsite_insn, estimated_caller_size,
                                      inlinable.insn_size, &caller_too_large_);
      }
    }
    if (not_inlinable) {
      if (caller_too_large_) {
        caller_too_large++;
      } else {
        calls_not_inlinable++;
      }
      continue;
    }

    TRACE(MMINL, 4, "%s",
          create_inlining_trace_msg(caller_method, callee_method, callsite_insn)
              .c_str());

    if (for_speed()) {
      std::lock_guard<std::mutex> lock(ab_exp_mutex);
      m_ab_experiment_context->try_register_method(caller_method);
    }

    if (m_config.unique_inlined_registers) {
      cfg_next_caller_reg = caller->cfg().get_registers_size();
    }
    auto timer2 = m_inline_with_cfg_timer.scope();
    auto it = m_inlined_invokes_need_cast.find(callsite_insn);
    auto needs_receiver_cast =
        it == m_inlined_invokes_need_cast.end() ? nullptr : it->second;
    auto needs_init_class = get_needs_init_class(callee_method);
    bool success = inliner::inline_with_cfg(
        caller_method, callee_method, callsite_insn, needs_receiver_cast,
        needs_init_class, *cfg_next_caller_reg, inlinable.reduced_cfg);
    if (!success) {
      calls_not_inlined++;
      continue;
    }
    TRACE(INL, 2, "caller: %s\tcallee: %s",
          caller->cfg_built() ? SHOW(caller->cfg()) : SHOW(caller),
          SHOW(callee));
    estimated_caller_size += inlinable.insn_size;
    if (inlinable.reduced_cfg) {
      visibility_changes.insert(get_visibility_changes(
          *inlinable.reduced_cfg, caller_method->get_class(), callee_method));
    } else {
      visibility_changes_for.insert(callee_method);
    }
    if (is_static(callee_method)) {
      init_classes++;
    }

    inlined_callees.push_back(callee_method);
    if (type::is_kotlin_lambda(type_class(callee_method->get_class()))) {
      info.kotlin_lambda_inlined++;
    }
  }

  if (!inlined_callees.empty()) {
    for (auto callee_method : visibility_changes_for) {
      visibility_changes.insert(
          get_visibility_changes(callee_method, caller_method->get_class()));
    }
    if (!visibility_changes.empty()) {
      std::lock_guard<std::mutex> guard(m_visibility_changes_mutex);
      if (m_delayed_visibility_changes) {
        m_delayed_visibility_changes->insert(visibility_changes);
      } else {
        visibility_changes_apply_and_record_make_static(visibility_changes);
      }
    }
    m_inlined.insert(inlined_callees.begin(), inlined_callees.end());
  }

  for (IRCode* code : need_deconstruct) {
    code->clear_cfg(nullptr, deleted_insns);
  }

  info.calls_inlined += inlined_callees.size();
  if (calls_not_inlinable) {
    info.calls_not_inlinable += calls_not_inlinable;
  }
  if (calls_not_inlined) {
    info.calls_not_inlined += calls_not_inlined;
  }
  if (no_returns) {
    info.no_returns += no_returns;
  }
  if (unreachable_insns) {
    info.unreachable_insns += unreachable_insns;
  }
  if (intermediate_shrinkings) {
    info.intermediate_shrinkings += intermediate_shrinkings;
  }
  if (intermediate_remove_unreachable_blocks) {
    info.intermediate_remove_unreachable_blocks +=
        intermediate_remove_unreachable_blocks;
  }
  if (caller_too_large) {
    info.caller_too_large += caller_too_large;
  }
  if (init_classes) {
    info.init_classes += init_classes;
  }
  return inlined_callees.size();
}