opt/outliner/InstructionSequenceOutliner.cpp (2,648 lines of code) (raw):
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/*
* This pass outlines common instruction sequences within a basic block,
* and across tree-shaped control-flow structures for size wins. The notion of
* instruction sequence equivalence is modulo register names.
*
* At its core is a rather naive approach: check if any subsequence of
* instructions in a block occurs sufficiently often. The average complexity is
* held down by filtering out instruction sequences where adjacent sequences of
* abstracted instructions ("cores") of fixed lengths never occur twice anywhere
* in the scope (seems good enough, even without a suffix tree).
*
* When reaching a conditional branch or switch instruction, different control-
* paths are explored as well, as long as they eventually all arrive at a common
* block. Thus, outline candidates are in fact instruction sequence trees.
*
* At its core is a rather naive approach: check if any subsequence of
* instructions in a block occurs sufficiently often. The average complexity is
* held down by filtering out instruction sequences where adjacent sequences of
* abstracted instructions ("cores") of fixed lengths never occur twice anywhere
* in the scope (seems good enough, even without a suffix tree).
*
* We gather existing method/type references in a dex and make sure that we
* don't go beyond the limits when adding methods/types, effectively filling up
* the available ref space created by IntraDexInline (minus other reservations).
*
* The pass assumes that it runs after InterDex, but before RegAlloc, and
* ideally before DedupStrings.
*
* There are some concessions to reduce the potential of negative runtime
* performance impact:
* - Performance sensitive methods (those that are popular in method-profiles,
* and loopy code in cold-start classes) are not outlined from
* - Outlining happens per dex to reduce performance impact (but then later
* dexes in the same store can point to outlined code in an earlier dex)
* - Outlined methods are preferably placed in the same class if all outlined
* sequences come from methods of a single class, or a common base class (the
* first one in the dex); otherwise, they are placed in a new shared helper
* classes (placed at the beginning of the dex)
* - DedupStrings pass will prefer to also use the same helper class
*
* Safety considerations:
* - Methods with non-minimum api levels are not outlined from.
* - Code involving cross-store refs is not outlined.
* - Many other technical limitations, similar in effect to inliner's technical
* limitations
*
* Ideas for future work:
* - Retain dex positions
* - More sophisticated normalization (commutative operations, re-ordering of
* independent instructions)
* - Make outlining a bit fuzzy (e.g. pulling out constants)
* - More aggressive cross-dex outlining
* - Other minor TODO ideas in the code
* - Outline beyond control-flow trees, e.g. DAGs, or even arbitrary
* control-flow with try/catches
*/
#include "InstructionSequenceOutliner.h"
#include <algorithm>
#include <list>
#include <map>
#include <memory>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include <boost/format.hpp>
#include "ABExperimentContext.h"
#include "BigBlocks.h"
#include "CFGMutation.h"
#include "ConcurrentContainers.h"
#include "ConfigFiles.h"
#include "Creators.h"
#include "Debug.h"
#include "DexClass.h"
#include "DexInstruction.h"
#include "DexLimits.h"
#include "DexPosition.h"
#include "DexUtil.h"
#include "IRCode.h"
#include "IRInstruction.h"
#include "InterDexPass.h"
#include "Lazy.h"
#include "Liveness.h"
#include "Macros.h"
#include "MethodProfiles.h"
#include "MutablePriorityQueue.h"
#include "OutlinedMethods.h"
#include "OutlinerTypeAnalysis.h"
#include "OutliningProfileGuidanceImpl.h"
#include "PartialCandidates.h"
#include "PassManager.h"
#include "ReachingInitializeds.h"
#include "RedexContext.h"
#include "RefChecker.h"
#include "Resolver.h"
#include "Show.h"
#include "StlUtil.h"
#include "Trace.h"
#include "Walkers.h"
using namespace outliner;
namespace {
using namespace outliner_impl;
constexpr const char* OUTLINED_CLASS_NAME_PREFIX = "Lcom/redex/Outlined$";
// Average cost of having an outlined method reference (method_id_item,
// proto_id, type_list, string) in code units.
const size_t COST_METHOD_METADATA = 8;
// Average cost of having an outlined method body (encoded_method, code_item) in
// code units.
const size_t COST_METHOD_BODY = 8;
// Overhead of calling an outlined method with a result (invoke + move-result).
const size_t COST_INVOKE_WITH_RESULT = 4;
// Overhead of calling an outlined method without a result.
const size_t COST_INVOKE_WITHOUT_RESULT = 3;
// Maximum number of arguments in outlined methods to avoid /range instructions
const size_t MAX_ARGS = 5;
// Minimum number of instructions to be outlined in a sequence, used in
// definition of cores
const size_t MIN_INSNS_SIZE = 3;
////////////////////////////////////////////////////////////////////////////////
// "Candidate instructions" with hashes, equality, and stable hashes
////////////////////////////////////////////////////////////////////////////////
// Here, the "core" of an instruction is its opcode and associated data such as
// method/field/string/type/data/literal. This "core" concept is used for
// pruning which instruction sequences occur multiple times. Used or defined
// registers are explicitly left out as those are getting normalized.
struct CandidateInstructionCore {
IROpcode opcode;
union {
DexMethodRef* method;
DexFieldRef* field;
const DexString* string;
DexType* type;
DexOpcodeData* data;
int64_t literal{0};
};
};
static size_t hash_value(const CandidateInstructionCore& cic) {
size_t hash = cic.opcode;
boost::hash_combine(hash, cic.literal);
return hash;
}
// We define "stable hashes" for instruction sequences to create rather unique
// and stable name string for the outlined methods --- essentially, the outlined
// method name characterizes the outlined instruction sequence. We want these
// names to be stable across Redex runs, and across different Redex (and there-
// fore also boost) versions, so that name-dependent PGO remains relatively
// meaningful even with outlining enabled.
using StableHash = uint64_t;
static StableHash stable_hash_value(const std::string& s) {
StableHash stable_hash{s.size()};
for (auto c : s) {
stable_hash = stable_hash * 3 + c;
}
return stable_hash;
}
static StableHash stable_hash_value(const CandidateInstructionCore& cic) {
StableHash stable_hash{cic.opcode};
switch (opcode::ref(cic.opcode)) {
case opcode::Ref::Method:
return stable_hash * 41 + stable_hash_value(show(cic.method));
case opcode::Ref::Field:
return stable_hash * 43 + stable_hash_value(show(cic.field));
case opcode::Ref::String:
return stable_hash * 47 + stable_hash_value(show(cic.string));
case opcode::Ref::Type:
return stable_hash * 53 + stable_hash_value(show(cic.type));
case opcode::Ref::Data:
return stable_hash * 59 + cic.data->size();
case opcode::Ref::Literal:
return stable_hash * 61 + cic.literal;
default:
return stable_hash;
}
}
using CandidateInstructionCoreHasher = boost::hash<CandidateInstructionCore>;
bool operator==(const CandidateInstructionCore& a,
const CandidateInstructionCore& b) {
return a.opcode == b.opcode && a.literal == b.literal;
}
struct CandidateInstruction {
CandidateInstructionCore core;
std::vector<reg_t> srcs;
boost::optional<reg_t> dest;
};
static size_t hash_value(const CandidateInstruction& ci) {
size_t hash = hash_value(ci.core);
boost::hash_combine(hash, boost::hash_range(ci.srcs.begin(), ci.srcs.end()));
if (ci.dest) {
boost::hash_combine(hash, *ci.dest);
}
return hash;
}
static StableHash stable_hash_value(const CandidateInstruction& ci) {
StableHash stable_hash = stable_hash_value(ci.core);
for (auto src : ci.srcs) {
stable_hash = stable_hash * 3 + src;
}
return stable_hash;
}
using CandidateInstructionHasher = boost::hash<CandidateInstruction>;
bool operator==(const CandidateInstruction& a, const CandidateInstruction& b) {
return a.core == b.core && a.srcs == b.srcs && a.dest == b.dest;
}
struct CandidateEdgeLabel {
cfg::EdgeType type;
cfg::Edge::MaybeCaseKey case_key;
};
bool operator==(const CandidateEdgeLabel& a, const CandidateEdgeLabel& b) {
return a.type == b.type && a.case_key == b.case_key;
}
bool operator!=(const CandidateEdgeLabel& a, const CandidateEdgeLabel& b) {
return !(a == b);
}
struct CandidateNode {
std::vector<CandidateInstruction> insns;
boost::optional<reg_t> res_reg;
std::vector<std::pair<CandidateEdgeLabel, std::shared_ptr<CandidateNode>>>
succs;
};
static size_t hash_value(const CandidateNode& cn) {
size_t hash = cn.insns.size();
if (cn.res_reg) {
boost::hash_combine(hash, *cn.res_reg);
}
boost::hash_combine(hash,
boost::hash_range(cn.insns.begin(), cn.insns.end()));
for (auto& p : cn.succs) {
boost::hash_combine(hash, *p.second);
}
return hash;
}
static StableHash stable_hash_value(const CandidateNode& cn) {
StableHash stable_hash{cn.insns.size()};
for (const auto& csi : cn.insns) {
stable_hash = stable_hash * 73 + stable_hash_value(csi);
}
if (cn.res_reg) {
stable_hash = stable_hash * 79 + *cn.res_reg;
}
for (auto& p : cn.succs) {
stable_hash = stable_hash * 199 + stable_hash_value(*p.second);
}
return stable_hash;
}
using CandidateNodeHasher = boost::hash<CandidateNode>;
bool operator!=(const CandidateNode& a, const CandidateNode& b);
bool operator==(const CandidateNode& a, const CandidateNode& b) {
if (a.insns != b.insns || a.res_reg != b.res_reg ||
a.succs.size() != b.succs.size()) {
return false;
}
for (size_t i = 0; i < a.succs.size(); i++) {
auto& p = a.succs.at(i);
auto& q = b.succs.at(i);
if (p.first != q.first || *p.second != *q.second) {
return false;
}
}
return true;
}
bool operator!=(const CandidateNode& a, const CandidateNode& b) {
return !(a == b);
}
struct Candidate {
std::vector<const DexType*> arg_types;
CandidateNode root;
const DexType* res_type{nullptr};
size_t size;
reg_t temp_regs;
};
static size_t hash_value(const Candidate& c) {
size_t hash = c.size;
boost::hash_combine(hash, c.root);
for (auto arg_type : c.arg_types) {
boost::hash_combine(hash, (size_t)arg_type);
}
boost::hash_combine(hash, (size_t)c.res_type);
return hash;
}
static StableHash stable_hash_value(const Candidate& c) {
StableHash stable_hash{c.arg_types.size()};
for (auto t : c.arg_types) {
stable_hash = stable_hash * 71 + stable_hash_value(show(t));
}
if (c.res_type) {
stable_hash = stable_hash * 73 + stable_hash_value(show(c.res_type));
}
stable_hash = stable_hash * 79 + stable_hash_value(c.root);
return stable_hash;
}
using CandidateHasher = boost::hash<Candidate>;
bool operator==(const Candidate& a, const Candidate& b) {
if (a.arg_types != b.arg_types || a.root != b.root ||
a.res_type != b.res_type) {
return false;
}
always_assert(a.size == b.size);
always_assert(a.temp_regs == b.temp_regs);
return true;
}
static CandidateInstructionCore to_core(IRInstruction* insn) {
CandidateInstructionCore core;
core.opcode = insn->opcode();
if (insn->has_method()) {
core.method = insn->get_method();
} else if (insn->has_field()) {
core.field = insn->get_field();
} else if (insn->has_string()) {
core.string = insn->get_string();
} else if (insn->has_type()) {
core.type = insn->get_type();
} else if (insn->has_literal()) {
core.literal = insn->get_literal();
} else if (insn->has_data()) {
core.data = insn->get_data();
}
return core;
}
using CandidateInstructionCores =
std::array<CandidateInstructionCore, MIN_INSNS_SIZE>;
using CandidateInstructionCoresHasher = boost::hash<CandidateInstructionCores>;
using CandidateInstructionCoresSet =
std::unordered_set<CandidateInstructionCores,
CandidateInstructionCoresHasher>;
// The cores builder efficiently keeps track of the last MIN_INSNS_SIZE many
// instructions.
class CandidateInstructionCoresBuilder {
public:
void push_back(IRInstruction* insn) {
m_buffer[m_start] = to_core(insn);
m_start = (m_start + 1) % MIN_INSNS_SIZE;
m_size = m_size < MIN_INSNS_SIZE ? m_size + 1 : MIN_INSNS_SIZE;
}
void clear() { m_size = 0; }
bool has_value() const { return m_size == MIN_INSNS_SIZE; }
CandidateInstructionCores get_value() {
always_assert(m_size == MIN_INSNS_SIZE);
CandidateInstructionCores res;
for (size_t i = 0; i < MIN_INSNS_SIZE; i++) {
res[i] = m_buffer.at((m_start + i) % MIN_INSNS_SIZE);
}
return res;
}
private:
std::array<CandidateInstructionCore, MIN_INSNS_SIZE> m_buffer;
size_t m_start{0};
size_t m_size{0};
};
////////////////////////////////////////////////////////////////////////////////
// Normalization of partial candidate sequence to candidate sequence
////////////////////////////////////////////////////////////////////////////////
using LazyReachingInitializedsEnvironments =
Lazy<reaching_initializeds::ReachingInitializedsEnvironments>;
using OptionalReachingInitializedsEnvironments =
boost::optional<reaching_initializeds::ReachingInitializedsEnvironments>;
static std::vector<cfg::Edge*> get_ordered_goto_and_branch_succs(
cfg::Block* block) {
std::vector<cfg::Edge*> succs =
block->cfg().get_succ_edges_if(block, [](cfg::Edge* e) {
return e->type() == cfg::EDGE_GOTO || e->type() == cfg::EDGE_BRANCH;
});
std::sort(succs.begin(), succs.end(), [](cfg::Edge* a, cfg::Edge* b) {
if (a->type() != b->type()) {
return a->type() < b->type();
}
return a->case_key() && *a->case_key() < *b->case_key();
});
return succs;
}
static CandidateEdgeLabel normalize(cfg::Edge* e) {
return CandidateEdgeLabel{e->type(), e->case_key()};
}
// This method turns a tree of actual instructions into a normalized
// candidate instruction tree. The main purpose of normalization is to
// determine a canonical register assignment. Normalization also identifies the
// list and types of incoming arguments. Normalized temporary registers start
// at zero, and normalized argument registers follow after temporary registers
// in the order in which they are referenced by the instructions when walking
// the tree in order.
static Candidate normalize(
OutlinerTypeAnalysis& type_analysis,
const PartialCandidate& pc,
const boost::optional<std::pair<reg_t, bool>>& out_reg) {
std::unordered_map<reg_t, reg_t> map;
reg_t next_arg{pc.temp_regs};
reg_t next_temp{0};
Candidate c;
c.size = pc.size;
c.temp_regs = pc.temp_regs;
std::vector<reg_t> arg_regs;
auto normalize_use = [&map, &next_arg, &arg_regs](reg_t reg, bool wide) {
auto it = map.find(reg);
if (it != map.end()) {
return it->second;
}
reg_t mapped_reg = next_arg;
next_arg += wide ? 2 : 1;
map.emplace(reg, mapped_reg);
arg_regs.push_back(reg);
return mapped_reg;
};
// We keep track of temp registers only needed along some
// (conditional) control-flow paths.
using UndoMap = std::unordered_map<reg_t, boost::optional<reg_t>>;
auto normalize_def = [&map, &next_temp](reg_t reg, bool wide,
UndoMap* undo_map) {
if (!undo_map->count(reg)) {
auto it = map.find(reg);
undo_map->emplace(reg, it == map.end()
? boost::none
: boost::optional<reg_t>(it->second));
}
reg_t mapped_reg = next_temp;
next_temp += wide ? 2 : 1;
map[reg] = mapped_reg;
return mapped_reg;
};
std::function<void(const PartialCandidateNode&, CandidateNode* cn,
IRInstruction* last_out_reg_assignment_insn)>
walk;
std::unordered_set<const IRInstruction*> out_reg_assignment_insns;
walk = [&map, &normalize_use, &normalize_def, &out_reg,
&out_reg_assignment_insns,
&walk](const PartialCandidateNode& pcn, CandidateNode* cn,
IRInstruction* last_out_reg_assignment_insn) {
UndoMap undo_map;
for (auto insn : pcn.insns) {
CandidateInstruction ci;
ci.core = to_core(insn);
for (size_t i = 0; i < insn->srcs_size(); i++) {
ci.srcs.push_back(normalize_use(insn->src(i), insn->src_is_wide(i)));
}
if (insn->has_dest()) {
ci.dest = normalize_def(insn->dest(), insn->dest_is_wide(), &undo_map);
if (out_reg && insn->dest() == out_reg->first) {
last_out_reg_assignment_insn = insn;
}
}
cn->insns.push_back(ci);
}
if (pcn.succs.empty() && out_reg) {
out_reg_assignment_insns.insert(last_out_reg_assignment_insn);
cn->res_reg = normalize_use(out_reg->first, out_reg->second);
}
for (auto& p : pcn.succs) {
auto succ_cn = std::make_shared<CandidateNode>();
cn->succs.emplace_back(normalize(p.first), succ_cn);
walk(*p.second, succ_cn.get(), last_out_reg_assignment_insn);
}
for (auto& p : undo_map) {
if (p.second) {
map[p.first] = *p.second;
} else {
map.erase(p.first);
}
}
};
walk(pc.root, &c.root, nullptr);
always_assert(next_temp == pc.temp_regs);
if (out_reg) {
always_assert(!out_reg_assignment_insns.empty());
if (out_reg_assignment_insns.count(nullptr)) {
// There is a control-flow path where the out-reg is not assigned;
// fall-back to type inference at the beginning of the partial candidate.
c.res_type = type_analysis.get_inferred_type(pc, out_reg->first);
out_reg_assignment_insns.erase(nullptr);
}
if (!out_reg_assignment_insns.empty()) {
c.res_type = type_analysis.get_result_type(&pc, out_reg_assignment_insns,
c.res_type);
}
}
for (auto reg : arg_regs) {
auto type = type_analysis.get_type_demand(
pc, reg, out_reg ? boost::optional<reg_t>(out_reg->first) : boost::none,
c.res_type);
c.arg_types.push_back(type);
}
return c;
}
////////////////////////////////////////////////////////////////////////////////
// find_method_candidate_sequences
////////////////////////////////////////////////////////////////////////////////
// A non-empty (ordered) map of range start positions to range sizes.
using Ranges = std::map<size_t, size_t>;
struct CandidateMethodLocation {
IRInstruction* first_insn;
cfg::Block* hint_block;
boost::optional<reg_t> out_reg;
// We use a linear instruction indexing scheme within a method to identify
// ranges, which we use later to invalidate other overlapping candidates
// while incrementally processing the most beneficial candidates using a
// priority queue.
Ranges ranges;
};
static bool ranges_overlap(const Ranges& a, const Ranges& b) {
auto range_begin = [](const Ranges::const_iterator& it) { return it->first; };
auto range_end = [](const Ranges::const_iterator& it) {
return it->first + it->second;
};
for (auto a_it = a.begin(), b_it = b.begin();
a_it != a.end() && b_it != b.end();) {
if (range_end(a_it) <= range_begin(b_it)) {
a_it++;
} else if (range_end(b_it) <= range_begin(a_it)) {
b_it++;
} else {
return true;
}
}
return false;
}
static bool can_outline_opcode(IROpcode opcode) {
switch (opcode) {
case IOPCODE_LOAD_PARAM:
case IOPCODE_LOAD_PARAM_OBJECT:
case IOPCODE_LOAD_PARAM_WIDE:
case OPCODE_GOTO:
case OPCODE_INVOKE_SUPER:
case OPCODE_MONITOR_ENTER:
case OPCODE_MONITOR_EXIT:
case OPCODE_MOVE_EXCEPTION:
case OPCODE_RETURN:
case OPCODE_RETURN_OBJECT:
case OPCODE_RETURN_VOID:
case OPCODE_RETURN_WIDE:
case OPCODE_THROW:
return false;
case OPCODE_CMPL_FLOAT:
case OPCODE_CMPG_FLOAT:
case OPCODE_CMPL_DOUBLE:
case OPCODE_CMPG_DOUBLE:
case OPCODE_CMP_LONG:
// While these instructions could formally be part of an outlined methods,
// we ran into issues in the past with the CSE pass, where breaking up
// CMP and IF instructions caused some obscure issues on some Android
// versions. So we rather avoid that. It's not a big loss.
return false;
default:
return true;
}
}
// Attempts to append an instruction to a partial candidate sequence. Result
// indicates whether attempt was successful. If not, then the partial
// candidate sequence should be abandoned.
static bool append_to_partial_candidate(
LazyReachingInitializedsEnvironments& reaching_initialized_new_instances,
IRInstruction* insn,
PartialCandidate* pc,
PartialCandidateNode* pcn) {
auto opcode = insn->opcode();
if (opcode == OPCODE_INVOKE_DIRECT && method::is_init(insn->get_method())) {
auto it = pcn->defined_regs.find(insn->src(0));
if (it == pcn->defined_regs.end()) {
return false;
}
it->second = {/* wide */ false, RegState::INITIALIZED};
}
for (size_t i = 0; i < insn->srcs_size(); i++) {
auto src = insn->src(i);
if (!pcn->defined_regs.count(src)) {
pc->in_regs.insert(src);
if (insn->src_is_wide(i)) {
pc->in_regs.insert(src + 1);
}
if (pc->in_regs.size() > MAX_ARGS) {
return false;
}
}
}
if (insn->has_dest()) {
DefinedReg defined_reg{insn->dest_is_wide(), RegState::INITIALIZED};
if (insn->opcode() == OPCODE_MOVE_OBJECT) {
auto it = pcn->defined_regs.find(insn->src(0));
if (it != pcn->defined_regs.end()) {
defined_reg = it->second;
} else {
auto initialized =
reaching_initialized_new_instances->at(insn).get(insn->src(0));
if (!initialized.get_constant() || !*initialized.get_constant()) {
return false;
}
}
} else if (opcode == IOPCODE_MOVE_RESULT_PSEUDO_OBJECT) {
always_assert(!pcn->insns.empty());
auto last_opcode = pcn->insns.back()->opcode();
if (last_opcode == OPCODE_NEW_INSTANCE) {
defined_reg.state = RegState::UNINITIALIZED;
}
}
pcn->defined_regs[insn->dest()] = defined_reg;
pc->temp_regs += insn->dest_is_wide() ? 2 : 1;
}
pcn->insns.push_back(insn);
pc->insns_size++;
if (!opcode::is_a_move(opcode)) {
// Moves are likely still eliminated by reg-alloc or other opts
if (insn->opcode() == IOPCODE_INIT_CLASS) {
pc->size += 2;
} else {
pc->size += insn->size();
}
}
return true;
}
static bool can_outline_insn(const RefChecker& ref_checker,
const OptionalReachingInitializedsEnvironments&
reaching_initialized_init_first_param,
IRInstruction* insn) {
if (!can_outline_opcode(insn->opcode())) {
return false;
}
if (insn->has_method()) {
auto method = resolve_method(insn->get_method(), opcode_to_search(insn));
if (method == nullptr || method != insn->get_method()) {
return false;
}
if (!is_public(method) && method->is_external()) {
return false;
}
if (!ref_checker.check_method(method)) {
return false;
}
auto rabbit_type =
DexType::make_type("Lcom/facebook/redex/RabbitRuntimeHelper;");
if (method->get_class() == rabbit_type) {
return false;
}
if (!PositionPatternSwitchManager::
CAN_OUTLINED_METHOD_INVOKE_OUTLINED_METHOD &&
insn->opcode() == OPCODE_INVOKE_STATIC && is_outlined_method(method)) {
// TODO: Remove this limitation imposed by symbolication infrastructure.
return false;
}
} else if (insn->has_field()) {
auto field = resolve_field(insn->get_field());
if (field == nullptr || field != insn->get_field()) {
return false;
}
if (!is_public(field) && field->is_external()) {
return false;
}
if (!ref_checker.check_field(field)) {
return false;
}
if (is_final(field) && (opcode::is_an_iput(insn->opcode()) ||
opcode::is_an_sput(insn->opcode()))) {
return false;
}
} else if (insn->has_type()) {
auto cls = type_class(insn->get_type());
if (cls != nullptr) {
if (!is_public(cls) && cls->is_external()) {
return false;
}
if (!ref_checker.check_type(insn->get_type())) {
return false;
}
}
}
if (reaching_initialized_init_first_param) {
// In a constructor, we cannot outline instructions that access the first
// parameter before the base constructor was called on it.
auto& env = reaching_initialized_init_first_param->at(insn);
for (size_t i = 0; i < insn->srcs_size(); i++) {
auto reg = insn->src(i);
const auto& initialized = env.get(reg);
if (!initialized.get_constant() || !*initialized.get_constant()) {
return false;
}
}
}
return true;
}
static bool is_uniquely_reached_via_pred(cfg::Block* block) {
auto& pred_edges = block->preds();
return pred_edges.size() == 1 && block != block->cfg().entry_block();
}
// Check if starting from the given iterator, we have a tree-shaped
// branching structure, and gather a common eventual target block where
// the leaf blocks would unconditionally go to.
static std::unordered_set<cfg::Block*> get_eventual_targets_after_outlining(
cfg::Block* first_block, const cfg::InstructionIterator& it) {
always_assert(opcode::is_a_conditional_branch(it->insn->opcode()) ||
opcode::is_switch(it->insn->opcode()));
auto get_targets =
[first_block](
cfg::Block* start_block) -> std::unordered_set<cfg::Block*> {
// The target could be the start block itself, if we don't find any other
// eligible targets, or the target(s) we find at the end of further
// conditional control-flow.
std::unordered_set<cfg::Block*> targets{start_block};
if (is_uniquely_reached_via_pred(start_block)) {
auto big_block = big_blocks::get_big_block(start_block);
if (big_block && big_block->same_try(first_block)) {
auto last_block = big_block->get_last_block();
auto last_insn_it = last_block->get_last_insn();
if (last_insn_it != last_block->end() &&
(opcode::is_a_conditional_branch(last_insn_it->insn->opcode()) ||
opcode::is_switch(last_insn_it->insn->opcode()))) {
auto last_insn_cfg_it =
last_block->to_cfg_instruction_iterator(last_insn_it);
auto more_targets = get_eventual_targets_after_outlining(
start_block, last_insn_cfg_it);
targets.insert(more_targets.begin(), more_targets.end());
} else {
auto goes_to = last_block->goes_to();
if (goes_to) {
targets.insert(goes_to);
}
}
}
}
return targets;
};
auto block = it.block();
auto succs = get_ordered_goto_and_branch_succs(block);
if (succs.empty()) {
return std::unordered_set<cfg::Block*>();
}
// We start with the targets of the first successor, and narrow them down
// with all other successor targets.
auto succs_it = succs.begin();
auto targets = get_targets((*succs_it)->target());
for (succs_it++; succs_it != succs.end() && !targets.empty(); succs_it++) {
auto other_targets = get_targets((*succs_it)->target());
std20::erase_if(targets, [&](auto* b) { return !other_targets.count(b); });
}
return targets;
}
using MethodCandidates =
std::unordered_map<Candidate,
std::vector<CandidateMethodLocation>,
CandidateHasher>;
// Callback invoked when either...
// - one more instruction was successfully appended to the partial candidate,
// and a point was reached that could potentially mark the end of an outlined
// instruction sequence (in this case, there is no next_block), or
// - the end of a converged fully explored conditional control-flow fork has
// been reached (in this case, the iterator is at the end, and next_block
// indicates the block at which the converged control-flow will continue).
using ExploredCallback =
std::function<void(PartialCandidate& pc,
cfg::Block* first_block,
const big_blocks::InstructionIterator& it,
cfg::Block* next_block)>;
// Look for and add entire candidate sequences starting at a
// particular point in a big block.
// Result indicates whether the big block was successfully explored to the end.
static bool explore_candidates_from(
LazyReachingInitializedsEnvironments& reaching_initialized_new_instances,
const OptionalReachingInitializedsEnvironments&
reaching_initialized_init_first_param,
const Config& config,
const RefChecker& ref_checker,
const CandidateInstructionCoresSet& recurring_cores,
PartialCandidate* pc,
PartialCandidateNode* pcn,
big_blocks::InstructionIterator it,
const big_blocks::InstructionIterator& end,
const ExploredCallback* explored_callback = nullptr) {
boost::optional<IROpcode> prev_opcode;
CandidateInstructionCoresBuilder cores_builder;
auto first_block = it.block();
auto& cfg = first_block->cfg();
for (; it != end; prev_opcode = it->insn->opcode(), it++) {
if (pc->insns_size >= config.max_insns_size) {
return false;
}
auto insn = it->insn;
if (pcn->insns.size() + 1 < MIN_INSNS_SIZE &&
!can_outline_insn(ref_checker, reaching_initialized_init_first_param,
insn)) {
return false;
}
cores_builder.push_back(insn);
if (cores_builder.has_value() &&
!recurring_cores.count(cores_builder.get_value())) {
return false;
}
if (!append_to_partial_candidate(reaching_initialized_new_instances, insn,
pc, pcn)) {
return false;
}
if (opcode::is_a_conditional_branch(insn->opcode()) ||
opcode::is_switch(insn->opcode())) {
// If the branching structure is such that there's a tree where all
// leaves nodes unconditionally goto a common block, then we'll attempt
// to gather a partial candidate tree.
// TODO: Handle not just conditional trees, but DAGs or even arbitrary
// control-flow.
auto targets =
get_eventual_targets_after_outlining(first_block, it.unwrap());
always_assert(targets.size() <= 1);
TRACE(ISO, 3, "[invoke sequence outliner] %zu eventual targets",
targets.size());
if (targets.size() != 1) {
return false;
}
auto block = it.block();
auto succs = get_ordered_goto_and_branch_succs(block);
auto defined_regs_copy = pcn->defined_regs;
pcn->defined_regs.clear();
auto next_block = *targets.begin();
for (auto succ_edge : succs) {
auto succ_pcn = std::make_shared<PartialCandidateNode>();
pcn->succs.emplace_back(succ_edge, succ_pcn);
succ_pcn->defined_regs = defined_regs_copy;
if (succ_edge->target() != next_block) {
auto succ_big_block = big_blocks::get_big_block(succ_edge->target());
always_assert(succ_big_block);
always_assert(succ_big_block->same_try(first_block));
always_assert(
is_uniquely_reached_via_pred(succ_big_block->get_first_block()));
auto succ_ii = big_blocks::InstructionIterable(*succ_big_block);
if (!explore_candidates_from(reaching_initialized_new_instances,
reaching_initialized_init_first_param,
config, ref_checker, recurring_cores, pc,
succ_pcn.get(), succ_ii.begin(),
succ_ii.end())) {
return false;
}
}
for (auto& q : succ_pcn->defined_regs) {
auto defined_regs_it = pcn->defined_regs.find(q.first);
if (defined_regs_it == pcn->defined_regs.end()) {
pcn->defined_regs.insert(q);
} else if (q.second.wide != defined_regs_it->second.wide) {
defined_regs_it->second.state = RegState::INCONSISTENT;
} else if (q.second.state < defined_regs_it->second.state) {
defined_regs_it->second.state = q.second.state;
}
}
}
if (explored_callback) {
(*explored_callback)(*pc, first_block, end, next_block);
}
return true;
}
// We cannot consider partial candidate sequences when they are
// missing their move-result piece
if (insn->has_move_result_any() &&
!cfg.move_result_of(it.unwrap()).is_end()) {
continue;
}
// We prefer not to consider sequences ending in const instructions.
// (Experiments have shown that outlining the overlapping candidate without
// a trailing consts tends to lead to better results and a faster outliner.)
if (insn->opcode() == OPCODE_CONST || insn->opcode() == OPCODE_CONST_WIDE ||
(insn->opcode() == IOPCODE_MOVE_RESULT_PSEUDO_OBJECT && prev_opcode &&
opcode::is_a_const(*prev_opcode))) {
continue;
}
if (explored_callback) {
(*explored_callback)(*pc, first_block, it, /* next_block */ nullptr);
}
}
return true;
}
#define STATS \
FOR_EACH(live_out_to_throw_edge) \
FOR_EACH(live_out_multiple) \
FOR_EACH(live_out_initialized_not) \
FOR_EACH(arg_type_not_computed) \
FOR_EACH(arg_type_illegal) \
FOR_EACH(res_type_not_computed) \
FOR_EACH(res_type_illegal) \
FOR_EACH(overlap) \
FOR_EACH(loop) \
FOR_EACH(block_warm_loop_exceeds_thresholds) \
FOR_EACH(block_warm_loop_no_source_blocks) \
FOR_EACH(block_hot) \
FOR_EACH(block_hot_exceeds_thresholds) \
FOR_EACH(block_hot_no_source_blocks)
struct FindCandidatesStats {
// NOLINTNEXTLINE(bugprone-macro-parentheses)
#define FOR_EACH(name) std::atomic<size_t> name{0};
STATS
#undef FOR_EACH
};
// For a single method, identify possible beneficial outlinable candidates.
// For each candidate, gather information about where exactly in the
// given method it is located.
static MethodCandidates find_method_candidates(
const Config& config,
const RefChecker& ref_checker,
const CanOutlineBlockDecider& block_decider,
DexMethod* method,
cfg::ControlFlowGraph& cfg,
const CandidateInstructionCoresSet& recurring_cores,
FindCandidatesStats* stats) {
MethodCandidates candidates;
Lazy<LivenessFixpointIterator> liveness_fp_iter([&cfg] {
auto res = std::make_unique<LivenessFixpointIterator>(cfg);
res->run({});
return res;
});
LazyUnorderedMap<cfg::Block*,
std::unordered_map<IRInstruction*, LivenessDomain>>
live_outs([&liveness_fp_iter](cfg::Block* block) {
std::unordered_map<IRInstruction*, LivenessDomain> res;
auto live_out = liveness_fp_iter->get_live_out_vars_at(block);
for (auto it = block->rbegin(); it != block->rend(); ++it) {
if (it->type != MFLOW_OPCODE) {
continue;
}
res.emplace(it->insn, live_out);
liveness_fp_iter->analyze_instruction(it->insn, &live_out);
}
return res;
});
// Variables that flow into a throw block, if any
LazyUnorderedMap<cfg::Block*, LivenessDomain> throw_live_out(
[&cfg, &liveness_fp_iter](cfg::Block* block) {
auto res = LivenessDomain::bottom();
for (auto e : cfg.get_succ_edges_of_type(block, cfg::EDGE_THROW)) {
res.join_with(liveness_fp_iter->get_live_in_vars_at(e->target()));
}
return res;
});
LazyReachingInitializedsEnvironments reaching_initialized_new_instances(
[&cfg]() {
return reaching_initializeds::get_reaching_initializeds(
cfg, reaching_initializeds::Mode::NewInstances);
});
OptionalReachingInitializedsEnvironments
reaching_initialized_init_first_param;
if (method::is_init(method)) {
reaching_initialized_init_first_param =
reaching_initializeds::get_reaching_initializeds(
cfg, reaching_initializeds::Mode::FirstLoadParam);
}
OutlinerTypeAnalysis type_analysis(method);
auto big_blocks = big_blocks::get_big_blocks(cfg);
// Along big blocks, we are assigning consecutive indices to instructions.
// We'll use this to manage ranges of instructions that need to get
// invalidated when overlapping ranges of insturctions are outlined.
Lazy<std::unordered_map<const IRInstruction*, size_t>> insn_idxes(
[&big_blocks]() {
std::unordered_map<const IRInstruction*, size_t> res;
for (auto& big_block : big_blocks) {
for (const auto& mie : big_blocks::InstructionIterable(big_block)) {
res.emplace(mie.insn, res.size());
}
}
return res;
});
struct {
#define FOR_EACH(name) size_t name{0};
STATS
#undef FOR_EACH
} lstats;
// We are visiting the instructions in this method in "big block" chunks:
// - The big blocks cover all blocks.
// - It is safe to do so as they all share the same throw-edges, and any
// outlined method invocation will be placed in the first block of the big
// block, with the appropriate throw edges.
for (auto& big_block : big_blocks) {
ExploredCallback explored_callback =
[&](PartialCandidate& pc,
cfg::Block* first_block,
const big_blocks::InstructionIterator& it,
cfg::Block* next_block) {
// At this point, we can consider the current candidate for
// normalization and outlining, adding it to the set of outlining
// candidates for this method
if (pc.insns_size < config.min_insns_size) {
// Code is below minimum configured size
return;
}
if (pc.size <= COST_INVOKE_WITHOUT_RESULT) {
// Partial candidate is not longer than the replacement invoke
// instruction would be
return;
}
std::vector<std::pair<reg_t, IRInstruction*>> live_out_consts;
boost::optional<std::pair<reg_t, bool>> out_reg;
if (!pc.root.defined_regs.empty()) {
LivenessDomain live_out;
if (next_block) {
live_out = liveness_fp_iter->get_live_in_vars_at(next_block);
} else {
live_out = live_outs[it.block()].at(it->insn);
}
auto first_block_throw_live_out = throw_live_out[first_block];
for (auto& p : pc.root.defined_regs) {
if (first_block_throw_live_out.contains(p.first)) {
TRACE(ISO, 4,
"[invoke sequence outliner] [bail out] Cannot return "
"value that's live-in to a throw edge");
lstats.live_out_to_throw_edge++;
return;
}
if (live_out.contains(p.first)) {
always_assert(p.second.state != RegState::INCONSISTENT);
if (out_reg) {
TRACE(ISO, 4,
"[invoke sequence outliner] [bail out] Cannot have "
"more than one out-reg");
lstats.live_out_multiple++;
return;
}
if (p.second.state == RegState::UNINITIALIZED) {
TRACE(ISO, 4,
"[invoke sequence outliner] [bail out] Cannot return "
"uninitialized");
lstats.live_out_initialized_not++;
return;
}
always_assert(p.second.state == RegState::INITIALIZED);
out_reg = std::make_pair(p.first, p.second.wide);
}
}
}
if (out_reg && pc.size <= COST_INVOKE_WITH_RESULT) {
// Code to outline is not longer than the replacement invoke
// instruction would be
return;
}
auto c = normalize(type_analysis, pc, out_reg);
if (std::find(c.arg_types.begin(), c.arg_types.end(), nullptr) !=
c.arg_types.end()) {
TRACE(ISO, 4,
"[invoke sequence outliner] [bail out] Could not infer "
"argument type");
lstats.arg_type_not_computed++;
return;
}
if (std::find_if(c.arg_types.begin(), c.arg_types.end(),
[&](const DexType* t) {
return !ref_checker.check_type(t);
}) != c.arg_types.end()) {
TRACE(ISO, 4,
"[invoke sequence outliner] [bail out] Illegal argument "
"type");
lstats.arg_type_illegal++;
return;
}
if (out_reg && c.res_type == nullptr) {
TRACE(ISO, 4,
"[invoke sequence outliner] [bail out] Could not infer "
"result type");
lstats.res_type_not_computed++;
return;
}
if (out_reg && !ref_checker.check_type(c.res_type)) {
TRACE(ISO, 4,
"[invoke sequence outliner] [bail out] Illegal result type");
lstats.res_type_illegal++;
return;
}
auto result = block_decider.can_outline_from_big_block(big_block);
if (result != CanOutlineBlockDecider::Result::CanOutline) {
// We could bail out on this way earlier, but doing it last gives us
// better statistics on what the damage really is
switch (result) {
case CanOutlineBlockDecider::Result::WarmLoop:
lstats.loop++;
return;
case CanOutlineBlockDecider::Result::WarmLoopExceedsThresholds:
lstats.block_warm_loop_exceeds_thresholds++;
return;
case CanOutlineBlockDecider::Result::WarmLoopNoSourceBlocks:
lstats.block_warm_loop_no_source_blocks++;
return;
case CanOutlineBlockDecider::Result::Hot:
lstats.block_hot++;
return;
case CanOutlineBlockDecider::Result::HotExceedsThresholds:
lstats.block_hot_exceeds_thresholds++;
return;
case CanOutlineBlockDecider::Result::HotNoSourceBlocks:
lstats.block_hot_no_source_blocks++;
return;
default:
not_reached();
}
}
auto& cmls = candidates[c];
std::map<size_t, size_t> ranges;
std::function<void(const PartialCandidateNode&)> insert_ranges;
insert_ranges = [&insert_ranges, &ranges,
&insn_idxes](const PartialCandidateNode& pcn) {
if (!pcn.insns.empty()) {
auto start = insn_idxes->at(pcn.insns.front());
auto size = pcn.insns.size();
ranges.emplace(start, size);
}
for (auto& p : pcn.succs) {
insert_ranges(*p.second);
}
};
insert_ranges(pc.root);
if (std::find_if(cmls.begin(), cmls.end(),
[&ranges](const CandidateMethodLocation& cml) {
return ranges_overlap(cml.ranges, ranges);
}) != cmls.end()) {
lstats.overlap++;
} else {
cmls.push_back((CandidateMethodLocation){
pc.root.insns.front(), first_block,
out_reg ? boost::optional<reg_t>(out_reg->first) : boost::none,
ranges});
}
};
auto ii = big_blocks::InstructionIterable(big_block);
for (auto it = ii.begin(), end = ii.end(); it != end; it++) {
if (opcode::is_move_result_any(it->insn->opcode())) {
// We cannot start a sequence at a move-result-any instruction
continue;
}
PartialCandidate pc;
explore_candidates_from(reaching_initialized_new_instances,
reaching_initialized_init_first_param, config,
ref_checker, recurring_cores, &pc, &pc.root, it,
end, &explored_callback);
}
}
#define FOR_EACH(name) \
if (lstats.name) stats->name += lstats.name;
STATS
#undef FOR_EACH
return candidates;
}
////////////////////////////////////////////////////////////////////////////////
// get_recurring_cores
////////////////////////////////////////////////////////////////////////////////
static bool can_outline_from_method(DexMethod* method) {
if (method->rstate.no_optimizations() || method->rstate.outlined()) {
return false;
}
return true;
}
// Gather set of recurring small (MIN_INSNS_SIZE) adjacent instruction
// sequences that are outlinable. Note that all longer recurring outlinable
// instruction sequences must be comprised of shorter recurring ones.
static void get_recurring_cores(
const Config& config,
PassManager& mgr,
const Scope& scope,
const std::unordered_set<DexMethod*>& sufficiently_warm_methods,
const std::unordered_set<DexMethod*>& sufficiently_hot_methods,
const RefChecker& ref_checker,
CandidateInstructionCoresSet* recurring_cores,
ConcurrentMap<DexMethod*, CanOutlineBlockDecider>* block_deciders) {
ConcurrentMap<CandidateInstructionCores, size_t,
CandidateInstructionCoresHasher>
concurrent_cores;
walk::parallel::code(
scope, [&config, &ref_checker, &sufficiently_warm_methods,
&sufficiently_hot_methods, &concurrent_cores,
block_deciders](DexMethod* method, IRCode& code) {
if (!can_outline_from_method(method)) {
return;
}
code.build_cfg(/* editable */ true);
code.cfg().calculate_exit_block();
CanOutlineBlockDecider block_decider(
config.profile_guidance, sufficiently_warm_methods.count(method),
sufficiently_hot_methods.count(method));
auto& cfg = code.cfg();
OptionalReachingInitializedsEnvironments
reaching_initialized_init_first_param;
if (method::is_init(method)) {
reaching_initialized_init_first_param =
reaching_initializeds::get_reaching_initializeds(
cfg, reaching_initializeds::Mode::FirstLoadParam);
}
for (auto& big_block : big_blocks::get_big_blocks(cfg)) {
if (block_decider.can_outline_from_big_block(big_block) !=
CanOutlineBlockDecider::Result::CanOutline) {
continue;
}
CandidateInstructionCoresBuilder cores_builder;
for (auto& mie : big_blocks::InstructionIterable(big_block)) {
auto insn = mie.insn;
if (!can_outline_insn(
ref_checker, reaching_initialized_init_first_param, insn)) {
cores_builder.clear();
continue;
}
cores_builder.push_back(insn);
if (cores_builder.has_value()) {
concurrent_cores.update(cores_builder.get_value(),
[](const CandidateInstructionCores&,
size_t& occurrences,
bool /* exists */) { occurrences++; });
}
}
}
block_deciders->emplace(method, std::move(block_decider));
});
size_t singleton_cores{0};
for (auto& p : concurrent_cores) {
always_assert(p.second > 0);
if (p.second > 1) {
recurring_cores->insert(p.first);
} else {
singleton_cores++;
}
}
mgr.incr_metric("num_singleton_cores", singleton_cores);
mgr.incr_metric("num_recurring_cores", recurring_cores->size());
TRACE(ISO, 2,
"[invoke sequence outliner] %zu singleton cores, %zu recurring "
"cores",
singleton_cores, recurring_cores->size());
}
////////////////////////////////////////////////////////////////////////////////
// get_beneficial_candidates
////////////////////////////////////////////////////////////////////////////////
struct CandidateInfo {
std::unordered_map<DexMethod*, std::vector<CandidateMethodLocation>> methods;
size_t count{0};
};
// We keep track of outlined methods that reside in earlier dexes of the current
// store. Order vector is used for keeping the track of the order of each
// candidate stored.
struct ReusableOutlinedMethods {
std::unordered_map<Candidate,
std::deque<std::pair<DexMethod*, std::set<uint32_t>>>,
CandidateHasher>
map;
std::vector<Candidate> order;
};
std::unordered_set<const DexType*> get_declaring_types(
const CandidateInfo& ci) {
std::unordered_set<const DexType*> types;
for (auto& p : ci.methods) {
types.insert(p.first->get_class());
}
return types;
}
// Helper function that enables quickly determinining if a class is a common
// base class of a set of class. To this end, it builds up a map that
// includes all transitive super classes associated with a count of how many of
// the original types share this ancestor. If the count is equal to the number
// of the original types, then it must be a common base class.
std::unordered_set<const DexType*> get_common_super_classes(
const std::unordered_set<const DexType*>& types) {
std::unordered_map<const DexType*, size_t> counted_super_classes;
std::unordered_set<const DexType*> common_super_classes;
for (auto t : types) {
do {
if (++counted_super_classes[t] == types.size()) {
common_super_classes.insert(t);
}
auto cls = type_class(t);
if (!cls) {
break;
}
t = cls->get_super_class();
} while (t != nullptr);
}
return common_super_classes;
}
std::unordered_set<const DexType*> get_super_classes(
const std::unordered_set<const DexType*>& types) {
std::unordered_set<const DexType*> super_classes;
for (auto t : types) {
do {
if (!super_classes.insert(t).second) {
break;
}
auto cls = type_class(t);
if (!cls) {
break;
}
t = cls->get_super_class();
} while (t != nullptr);
}
return super_classes;
}
// Given a candidate, find all referenced types which will get get
// unconditionally initialized when the instructions run. In case of multiple
// successors, only types in common by all successors are considered. The result
// may include super types.
std::unordered_set<const DexType*> get_referenced_types(const Candidate& c) {
std::function<std::unordered_set<const DexType*>(const CandidateNode& cn)>
gather_types;
gather_types = [&gather_types](const CandidateNode& cn) {
std::unordered_set<const DexType*> types;
auto insert_internal_type = [&types](const DexType* t) {
auto cls = type_class(t);
if (cls && !cls->is_external()) {
types.insert(t);
}
};
for (auto& csi : cn.insns) {
auto& cic = csi.core;
switch (opcode::ref(cic.opcode)) {
case opcode::Ref::Method:
insert_internal_type(cic.method->get_class());
break;
case opcode::Ref::Field:
insert_internal_type(cic.field->get_class());
break;
case opcode::Ref::Type:
// in particular, let's exclude const-class here, as that doesn't ensure
// that the static initializer will run
if (cic.opcode == OPCODE_NEW_INSTANCE) {
insert_internal_type(cic.type);
}
break;
default:
break;
}
}
if (!cn.succs.empty()) {
std::unordered_map<const DexType*, size_t> successor_types;
for (auto& p : cn.succs) {
for (auto type : get_super_classes(gather_types(*p.second))) {
if (++successor_types[type] == cn.succs.size()) {
types.insert(type);
}
}
}
}
return types;
};
return gather_types(c.root);
}
// Finds an outlined method that either resides in an outlined helper class, or
// a common base class, or is co-located with its references.
static DexMethod* find_reusable_method(
const Candidate& c,
const CandidateInfo& ci,
const ReusableOutlinedMethods& outlined_methods,
bool reuse_outlined_methods_across_dexes) {
if (!reuse_outlined_methods_across_dexes) {
// reuse_outlined_methods_across_dexes is disabled
return nullptr;
}
auto it = outlined_methods.map.find(c);
if (it == outlined_methods.map.end()) {
return nullptr;
}
auto& method_pattern_pairs = it->second;
DexMethod* helper_class_method{nullptr};
for (const auto& vec_entry : method_pattern_pairs) {
auto method = vec_entry.first;
auto cls = type_class(method->get_class());
if (cls->rstate.outlined()) {
helper_class_method = method;
continue;
}
auto declaring_types = get_declaring_types(ci);
auto common_super_classes = get_common_super_classes(declaring_types);
if (common_super_classes.count(method->get_class())) {
return method;
}
auto referenced_types = get_referenced_types(c);
auto super_classes = get_super_classes(referenced_types);
if (super_classes.count(method->get_class())) {
return method;
}
}
return helper_class_method;
}
static size_t get_savings(const Config& config,
const Candidate& c,
const CandidateInfo& ci,
const ReusableOutlinedMethods& outlined_methods) {
size_t cost = c.size * ci.count;
size_t outlined_cost =
COST_METHOD_METADATA +
(c.res_type ? COST_INVOKE_WITH_RESULT : COST_INVOKE_WITHOUT_RESULT) *
ci.count;
if (find_reusable_method(c, ci, outlined_methods,
config.reuse_outlined_methods_across_dexes) ==
nullptr) {
outlined_cost += COST_METHOD_BODY + c.size;
}
return (outlined_cost + config.savings_threshold) < cost
? (cost - outlined_cost)
: 0;
}
using CandidateId = uint32_t;
struct CandidateWithInfo {
Candidate candidate;
CandidateInfo info;
};
// Find beneficial candidates across all methods. Beneficial candidates are
// those that occur often enough so that there would be a net savings (in terms
// of code units / bytes) when outlining them.
//
// We distinguish three kinds of methods:
// - "hot" methods are known to get invoked many times, and we do not outline
// from them.
// - "warm" methods are known to get invoked only a few times, and we will not
// outline from loops within them.
// - all other methods are considered to be cold, and every outlining
// opportunity in them will be considered.
//
// Candidates are identified by numerical candidate ids to make things
// deterministic (as opposed to a pointer) and provide an efficient
// identification mechanism.
static void get_beneficial_candidates(
const Config& config,
PassManager& mgr,
const Scope& scope,
const RefChecker& ref_checker,
const CandidateInstructionCoresSet& recurring_cores,
const ConcurrentMap<DexMethod*, CanOutlineBlockDecider>& block_deciders,
const ReusableOutlinedMethods* outlined_methods,
std::vector<CandidateWithInfo>* candidates_with_infos,
std::unordered_map<DexMethod*, std::unordered_set<CandidateId>>*
candidate_ids_by_methods) {
ConcurrentMap<Candidate, CandidateInfo, CandidateHasher>
concurrent_candidates;
FindCandidatesStats stats;
walk::parallel::code(scope, [&config, &ref_checker, &recurring_cores,
&concurrent_candidates, &block_deciders,
&stats](DexMethod* method, IRCode& code) {
if (!can_outline_from_method(method)) {
return;
}
for (auto& p : find_method_candidates(
config, ref_checker, block_deciders.at_unsafe(method), method,
code.cfg(), recurring_cores, &stats)) {
std::vector<CandidateMethodLocation>& cmls = p.second;
concurrent_candidates.update(p.first,
[method, &cmls](const Candidate&,
CandidateInfo& info,
bool /* exists */) {
info.methods.emplace(method, cmls);
info.count += cmls.size();
});
}
});
#define FOR_EACH(name) mgr.incr_metric("num_candidate_" #name, stats.name);
STATS
#undef FOR_EACH
using CandidateSet = std::unordered_set<Candidate, CandidateHasher>;
std::map<DexMethod*, CandidateSet, dexmethods_comparator>
candidates_by_methods;
size_t beneficial_count{0}, maleficial_count{0};
for (auto& p : concurrent_candidates) {
if (get_savings(config, p.first, p.second, *outlined_methods) > 0) {
beneficial_count += p.second.count;
for (auto& q : p.second.methods) {
candidates_by_methods[q.first].insert(p.first);
}
} else {
maleficial_count += p.second.count;
}
}
TRACE(ISO, 2,
"[invoke sequence outliner] %zu beneficial candidates, %zu "
"maleficial candidates",
beneficial_count, maleficial_count);
mgr.incr_metric("num_beneficial_candidates", beneficial_count);
mgr.incr_metric("num_maleficial_candidates", maleficial_count);
// Deterministically compute unique candidate ids
std::unordered_map<Candidate, CandidateId, CandidateHasher> candidate_ids;
for (auto& p : candidates_by_methods) {
auto& method_candidate_ids = (*candidate_ids_by_methods)[p.first];
std::vector<std::pair<CandidateMethodLocation, CandidateSet::iterator>>
ordered;
for (auto it = p.second.begin(); it != p.second.end(); it++) {
auto& c = *it;
auto id_it = candidate_ids.find(c);
if (id_it != candidate_ids.end()) {
method_candidate_ids.insert(id_it->second);
continue;
}
const auto& ci = concurrent_candidates.at_unsafe(c);
for (auto& cml : ci.methods.at(p.first)) {
ordered.emplace_back(cml, it);
}
}
std::sort(
ordered.begin(), ordered.end(),
[](const std::pair<CandidateMethodLocation, CandidateSet::iterator>& a,
const std::pair<CandidateMethodLocation, CandidateSet::iterator>&
b) {
auto& a_ranges = a.first.ranges;
auto& b_ranges = b.first.ranges;
if (a_ranges.size() != b_ranges.size()) {
return a_ranges.size() < b_ranges.size();
}
for (auto a_it = a_ranges.begin(), b_it = b_ranges.begin();
a_it != a_ranges.end(); a_it++, b_it++) {
if (a_it->first != b_it->first) {
return a_it->first < b_it->first;
}
if (a_it->second != b_it->second) {
return a_it->second < b_it->second;
}
}
always_assert(a.first.first_insn == b.first.first_insn);
always_assert(a.first.hint_block == b.first.hint_block);
always_assert(a.second == b.second);
return false;
});
for (auto& q : ordered) {
auto& c = *q.second;
if (!candidate_ids.count(c)) {
always_assert(candidate_ids.size() < (1ULL << 32));
CandidateId candidate_id = candidate_ids.size();
method_candidate_ids.insert(candidate_id);
candidate_ids.emplace(c, candidate_id);
candidates_with_infos->push_back(
{c, concurrent_candidates.at_unsafe(c)});
}
}
}
}
////////////////////////////////////////////////////////////////////////////////
// outline
////////////////////////////////////////////////////////////////////////////////
static bool has_non_init_invoke_directs(const CandidateNode& cn) {
for (const auto& ci : cn.insns) {
if (ci.core.opcode == OPCODE_INVOKE_DIRECT &&
!method::is_init(ci.core.method)) {
return true;
}
}
for (auto& p : cn.succs) {
if (has_non_init_invoke_directs(*p.second)) {
return true;
}
}
return false;
}
static bool has_non_init_invoke_directs(const Candidate& c) {
return has_non_init_invoke_directs(c.root);
}
// A name generator for outlined methods
class MethodNameGenerator {
private:
PassManager& m_mgr;
std::unordered_map<StableHash, size_t> m_unique_method_ids;
size_t m_max_unique_method_id{0};
size_t m_iteration;
public:
MethodNameGenerator() = delete;
MethodNameGenerator(const MethodNameGenerator&) = delete;
MethodNameGenerator& operator=(const MethodNameGenerator&) = delete;
explicit MethodNameGenerator(PassManager& mgr, size_t iteration)
: m_mgr(mgr), m_iteration(iteration) {}
// Compute the name of the outlined method in a way that tends to be stable
// across Redex runs.
const DexString* get_name(const Candidate& c) {
StableHash stable_hash = stable_hash_value(c);
auto unique_method_id = m_unique_method_ids[stable_hash]++;
m_max_unique_method_id = std::max(m_max_unique_method_id, unique_method_id);
std::string name(OUTLINED_METHOD_NAME_PREFIX);
name += std::to_string(m_iteration) + "$";
name += (boost::format("%08x") % stable_hash).str();
if (unique_method_id > 0) {
name += std::string("$") + std::to_string(unique_method_id);
TRACE(ISO, 5,
"[invoke sequence outliner] name with non-unique stable id: %s",
name.c_str());
}
return DexString::make_string(name);
}
~MethodNameGenerator() {
m_mgr.incr_metric("max_unique_method_id", m_max_unique_method_id);
TRACE(ISO, 2, "[invoke sequence outliner] %zu max unique method id",
m_max_unique_method_id);
}
};
// This mimics what generate_debug_instructions(DexClass.cpp) does eventually:
// ignoring positions that don't have a file.
static DexPosition* skip_fileless(DexPosition* pos) {
for (; pos && !pos->file; pos = pos->parent) {
}
return pos;
}
using CallSitePatternIds = std::unordered_map<IRInstruction*, uint32_t>;
class OutlinedMethodCreator {
private:
const Config& m_config;
PassManager& m_mgr;
MethodNameGenerator& m_method_name_generator;
size_t m_outlined_methods{0};
CallSitePatternIds m_call_site_pattern_ids;
std::unordered_map<DexMethod*, std::unique_ptr<DexPosition>>
m_unique_entry_positions;
// Gets the set of unique dbg-position patterns.
std::set<uint32_t> get_outlined_dbg_positions_patterns(
const Candidate& c, const CandidateInfo& ci) {
std::set<uint32_t> pattern_ids;
add_outlined_dbg_position_patterns(c, ci, &pattern_ids);
always_assert(!pattern_ids.empty());
if (!m_config.full_dbg_positions) {
// Find the "best" representative set of debug position, that is the one
// which provides most detail, i.e. has the highest number of unique debug
// positions
uint32_t best_pattern_id = *pattern_ids.begin();
size_t best_unique_positions{0};
auto manager = g_redex->get_position_pattern_switch_manager();
const auto& all_managed_patterns = manager->get_patterns();
for (auto pattern_id : pattern_ids) {
std::unordered_set<DexPosition*> unique_positions;
for (auto pos : all_managed_patterns.at(pattern_id)) {
unique_positions.insert(pos);
}
if (unique_positions.size() > best_unique_positions) {
best_pattern_id = pattern_id;
best_unique_positions = unique_positions.size();
}
}
return {best_pattern_id};
}
return pattern_ids;
}
public:
OutlinedMethodCreator() = delete;
OutlinedMethodCreator(const OutlinedMethodCreator&) = delete;
OutlinedMethodCreator& operator=(const OutlinedMethodCreator&) = delete;
explicit OutlinedMethodCreator(const Config& config,
PassManager& mgr,
MethodNameGenerator& method_name_generator)
: m_config(config),
m_mgr(mgr),
m_method_name_generator(method_name_generator) {}
// Infers outlined pattern ids.
void add_outlined_dbg_position_patterns(const Candidate& c,
const CandidateInfo& ci,
std::set<uint32_t>* pattern_ids) {
auto manager = g_redex->get_position_pattern_switch_manager();
// Order methods to make sure we get deterministic pattern-ids.
std::vector<DexMethod*> ordered_methods;
for (auto& p : ci.methods) {
ordered_methods.push_back(p.first);
}
std::sort(ordered_methods.begin(), ordered_methods.end(),
compare_dexmethods);
for (auto method : ordered_methods) {
auto& cmls = ci.methods.at(method);
for (auto& cml : cmls) {
// if the current method is reused then the call site
// didn't have the pattern id. Need to create and add pattern_id
auto positions = get_outlined_dbg_positions(c, cml, method);
auto pattern_id = manager->make_pattern(positions);
if (m_config.full_dbg_positions) {
m_call_site_pattern_ids.emplace(cml.first_insn, pattern_id);
}
pattern_ids->insert(pattern_id);
}
}
}
// Obtain outlined method for a candidate.
DexMethod* create_outlined_method(const Candidate& c,
const CandidateInfo& ci,
const DexType* host_class,
std::set<uint32_t>* pattern_ids) {
auto name = m_method_name_generator.get_name(c);
DexTypeList::ContainerType arg_types;
for (auto t : c.arg_types) {
arg_types.push_back(const_cast<DexType*>(t));
}
auto rtype = c.res_type ? c.res_type : type::_void();
auto type_list = DexTypeList::make_type_list(std::move(arg_types));
auto proto = DexProto::make_proto(rtype, type_list);
auto outlined_method =
DexMethod::make_method(const_cast<DexType*>(host_class), name, proto)
->make_concrete(ACC_PUBLIC | ACC_STATIC, false);
// get pattern ids
*pattern_ids = get_outlined_dbg_positions_patterns(c, ci);
always_assert(!(*pattern_ids).empty());
// not setting method body here
outlined_method->set_deobfuscated_name(show(outlined_method));
outlined_method->rstate.set_dont_inline();
outlined_method->rstate.set_outlined();
// not setting visibility here
type_class(host_class)->add_method(outlined_method);
TRACE(ISO, 5, "[invoke sequence outliner] outlined to %s",
SHOW(outlined_method));
m_outlined_methods++;
return outlined_method;
}
CallSitePatternIds* get_call_site_pattern_ids() {
if (m_config.full_dbg_positions) {
return &m_call_site_pattern_ids;
} else {
always_assert(m_call_site_pattern_ids.empty());
return nullptr;
}
}
PositionPattern get_outlined_dbg_positions(const Candidate& c,
const CandidateMethodLocation& cml,
DexMethod* method) {
auto& cfg = method->get_code()->cfg();
auto root_insn_it = cfg.find_insn(cml.first_insn, cml.hint_block);
if (root_insn_it.is_end()) {
// This should not happen, as for each candidate we never produce
// overlapping locations in a method, and overlaps across selected
// candidates are prevented by meticulously removing remaining overlapping
// occurrences after processing a candidate.
not_reached();
}
PositionPattern positions;
auto root_dbg_pos = skip_fileless(cfg.get_dbg_pos(root_insn_it));
if (!root_dbg_pos) {
if (!m_config.full_dbg_positions) {
return positions;
}
// We'll provide a "synthetic entry position" as the root. Copies of that
// position will be made later when it's actually needed. For now, we just
// need to obtain a template instance, and we need to store it somewhere
// so that we don't leak it.
auto it = m_unique_entry_positions.find(method);
if (it == m_unique_entry_positions.end()) {
it = m_unique_entry_positions
.emplace(method,
DexPosition::make_synthetic_entry_position(method))
.first;
}
root_dbg_pos = it->second.get();
TRACE(ISO, 6,
"[instruction sequence outliner] using synthetic position for "
"outlined code within %s",
SHOW(method));
}
std::function<void(DexPosition * dbg_pos, const CandidateNode& cn,
big_blocks::Iterator it)>
walk;
walk = [&positions, &walk](DexPosition* dbg_pos,
const CandidateNode& cn,
big_blocks::Iterator it) {
cfg::Block* last_block{nullptr};
for (auto& csi : cn.insns) {
for (; it->type == MFLOW_POSITION || it->type == MFLOW_DEBUG ||
it->type == MFLOW_SOURCE_BLOCK;
it++) {
if (it->type == MFLOW_POSITION && it->pos->file) {
dbg_pos = it->pos.get();
}
}
always_assert(it->type == MFLOW_OPCODE);
always_assert(it->insn->opcode() == csi.core.opcode);
positions.push_back(dbg_pos);
last_block = it.block();
it++;
}
if (!cn.succs.empty()) {
always_assert(last_block != nullptr);
auto succs = get_ordered_goto_and_branch_succs(last_block);
always_assert(succs.size() == cn.succs.size());
for (size_t i = 0; i < succs.size(); i++) {
always_assert(normalize(succs.at(i)) == cn.succs.at(i).first);
auto& succ_cn = *cn.succs.at(i).second;
auto succ_block = succs.at(i)->target();
auto succ_it = big_blocks::Iterator(succ_block, succ_block->begin());
walk(dbg_pos, succ_cn, succ_it);
}
}
};
walk(root_dbg_pos, c.root,
big_blocks::Iterator(root_insn_it.block(), root_insn_it.unwrap(),
/* ignore_throws */ true));
return positions;
}
~OutlinedMethodCreator() {
m_mgr.incr_metric("num_outlined_methods", m_outlined_methods);
TRACE(ISO, 2,
"[instruction sequence outliner] %zu outlined methods created",
m_outlined_methods);
}
};
// Rewrite instructions in existing method to invoke an outlined
// method instead.
static void rewrite_at_location(DexMethod* outlined_method,
const CallSitePatternIds* call_site_pattern_ids,
DexMethod* method,
cfg::ControlFlowGraph& cfg,
const Candidate& c,
const CandidateMethodLocation& cml) {
// Figure out argument and result registers
auto first_insn_it = cfg.find_insn(cml.first_insn, cml.hint_block);
if (first_insn_it.is_end()) {
// This should not happen, as for each candidate we never produce
// overlapping locations in a method, and overlaps across selected
// candidates are prevented by meticulously removing remaining overlapping
// occurrences after processing a candidate.
not_reached();
}
auto code = method->get_code();
if (code->get_debug_item() == nullptr) {
// Create an empty item so that debug info of method we are outlining from
// does not get lost.
code->set_debug_item(std::make_unique<DexDebugItem>());
// Create a synthetic initial position.
cfg.insert_before(cfg.entry_block(),
cfg.entry_block()->get_first_non_param_loading_insn(),
DexPosition::make_synthetic_entry_position(method));
TRACE(ISO, 6,
"[instruction sequence outliner] setting debug item and synthetic "
"entry position in %s",
SHOW(method));
}
auto last_dbg_pos = skip_fileless(cfg.get_dbg_pos(first_insn_it));
cfg::CFGMutation cfg_mutation(cfg);
const auto first_arg_reg = c.temp_regs;
boost::optional<reg_t> last_arg_reg;
std::vector<reg_t> arg_regs;
std::function<void(const CandidateNode& cn,
big_blocks::InstructionIterator it)>
walk;
auto gather_arg_regs = [first_arg_reg, &last_arg_reg,
&arg_regs](reg_t mapped_reg, reg_t reg) {
if (mapped_reg >= first_arg_reg &&
(!last_arg_reg || mapped_reg > *last_arg_reg)) {
last_arg_reg = mapped_reg;
arg_regs.push_back(reg);
}
};
walk = [&walk, &last_dbg_pos, &gather_arg_regs, &cml, &cfg, &cfg_mutation](
const CandidateNode& cn, big_blocks::InstructionIterator it) {
cfg::Block* last_block{nullptr};
boost::optional<cfg::InstructionIterator> last_insn_it;
for (size_t insn_idx = 0; insn_idx < cn.insns.size();
last_block = it.block(), last_insn_it = it.unwrap(), insn_idx++,
it++) {
auto& ci = cn.insns.at(insn_idx);
always_assert(it->insn->opcode() == ci.core.opcode);
for (size_t i = 0; i < ci.srcs.size(); i++) {
gather_arg_regs(ci.srcs.at(i), it->insn->src(i));
}
if (!opcode::is_move_result_any(it->insn->opcode())) {
cfg_mutation.remove(it.unwrap());
}
}
if (cn.succs.empty()) {
if (cn.res_reg) {
gather_arg_regs(*cn.res_reg, *cml.out_reg);
}
if (last_insn_it) {
auto dbg_pos = skip_fileless(cfg.get_dbg_pos(*last_insn_it));
if (dbg_pos) {
last_dbg_pos = dbg_pos;
}
}
} else {
always_assert(last_block != nullptr);
auto succs = get_ordered_goto_and_branch_succs(last_block);
always_assert(succs.size() == cn.succs.size());
for (size_t i = 0; i < succs.size(); i++) {
always_assert(normalize(succs.at(i)) == cn.succs.at(i).first);
auto succ_cn = *cn.succs.at(i).second;
auto succ_block = succs.at(i)->target();
auto succ_ii = InstructionIterable(succ_block);
auto succ_it = big_blocks::InstructionIterator(
succ_block->to_cfg_instruction_iterator(succ_ii.begin()));
walk(succ_cn, succ_it);
}
}
};
walk(c.root, big_blocks::InstructionIterator(first_insn_it,
/* ignore_throws */ true));
// Generate and insert invocation instructions
std::vector<IRInstruction*> outlined_method_invocation;
IRInstruction* invoke_insn = new IRInstruction(OPCODE_INVOKE_STATIC);
invoke_insn->set_method(outlined_method);
invoke_insn->set_srcs_size(arg_regs.size());
for (size_t i = 0; i < arg_regs.size(); i++) {
invoke_insn->set_src(i, arg_regs.at(i));
}
outlined_method_invocation.push_back(invoke_insn);
IRInstruction* move_result_insn = nullptr;
if (c.res_type) {
move_result_insn =
new IRInstruction(opcode::move_result_for_invoke(outlined_method));
move_result_insn->set_dest(*cml.out_reg);
outlined_method_invocation.push_back(move_result_insn);
}
cfg_mutation.insert_before(first_insn_it, outlined_method_invocation);
std::unique_ptr<DexPosition> new_dbg_pos;
if (call_site_pattern_ids) {
auto manager = g_redex->get_position_pattern_switch_manager();
auto pattern_id = call_site_pattern_ids->at(cml.first_insn);
new_dbg_pos = manager->make_pattern_position(pattern_id);
} else {
new_dbg_pos = std::make_unique<DexPosition>(0);
new_dbg_pos->bind(DexString::make_string(show(outlined_method)),
DexString::make_string("RedexGenerated"));
}
cfg_mutation.insert_before(first_insn_it, std::move(new_dbg_pos));
if (last_dbg_pos) {
new_dbg_pos = std::make_unique<DexPosition>(*last_dbg_pos);
} else {
new_dbg_pos = DexPosition::make_synthetic_entry_position(method);
TRACE(
ISO, 6,
"[instruction sequence outliner] reverting to synthetic position in %s",
SHOW(method));
}
cfg_mutation.insert_after(first_insn_it, std::move(new_dbg_pos));
cfg_mutation.flush();
}
// Manages references and assigns numeric ids to classes
// We don't want to use more methods or types than are available, so we gather
// all already used references in the given scope.
class DexState {
private:
PassManager& m_mgr;
DexClasses& m_dex;
size_t m_dex_id;
std::unordered_set<const DexType*> m_type_refs;
size_t m_method_refs_count;
std::unordered_map<const DexType*, size_t> m_class_ids;
size_t max_type_refs;
public:
DexState() = delete;
DexState(const DexState&) = delete;
DexState& operator=(const DexState&) = delete;
DexState(PassManager& mgr,
const init_classes::InitClassesWithSideEffects&
init_classes_with_side_effects,
DexClasses& dex,
size_t dex_id,
size_t reserved_trefs,
size_t reserved_mrefs)
: m_mgr(mgr), m_dex(dex), m_dex_id(dex_id) {
std::unordered_set<DexMethodRef*> method_refs;
std::vector<DexType*> init_classes;
for (auto cls : dex) {
cls->gather_methods(method_refs);
cls->gather_types(m_type_refs);
cls->gather_init_classes(init_classes);
}
m_method_refs_count = method_refs.size() + reserved_mrefs;
walk::classes(dex, [&class_ids = m_class_ids](DexClass* cls) {
class_ids.emplace(cls->get_type(), class_ids.size());
});
std::unordered_set<DexType*> refined_types;
for (auto type : init_classes) {
auto refined_type = init_classes_with_side_effects.refine(type);
if (refined_type) {
m_type_refs.insert(const_cast<DexType*>(refined_type));
}
}
max_type_refs =
get_max_type_refs(m_mgr.get_redex_options().min_sdk) - reserved_trefs;
}
size_t get_dex_id() { return m_dex_id; }
bool can_insert_type_refs(const std::unordered_set<const DexType*>& types) {
size_t inserted_count{0};
for (auto t : types) {
if (!m_type_refs.count(t)) {
inserted_count++;
}
}
// Yes, looks a bit quirky, but matching what happens in
// InterDex/DexStructure: The number of type refs must stay *below* the
// maximum, and must never reach it.
if ((m_type_refs.size() + inserted_count) >= max_type_refs) {
m_mgr.incr_metric("kMaxTypeRefs", 1);
TRACE(ISO, 2, "[invoke sequence outliner] hit kMaxTypeRefs");
return false;
}
return true;
}
void insert_type_refs(const std::unordered_set<const DexType*>& types) {
always_assert(can_insert_type_refs(types));
m_type_refs.insert(types.begin(), types.end());
always_assert(m_type_refs.size() < max_type_refs);
}
bool can_insert_method_ref() {
if (m_method_refs_count >= kMaxMethodRefs) {
m_mgr.incr_metric("kMaxMethodRefs", 1);
TRACE(ISO, 2, "[invoke sequence outliner] hit kMaxMethodRefs");
return false;
}
return true;
}
void insert_method_ref() {
always_assert(can_insert_method_ref());
m_method_refs_count++;
always_assert(m_method_refs_count <= kMaxMethodRefs);
}
// insert at beginning of dex, but after canary class, if any
void insert_outlined_class(DexClass* outlined_cls) {
auto it = m_dex.begin();
for (; it != m_dex.end() &&
(interdex::is_canary(*it) || (*it)->rstate.outlined());
it++) {
}
m_dex.insert(it, outlined_cls);
}
// Class ids represent the position of a class in the dex; we use this to
// determine if class in the dex, which one comes first, when deciding
// on a host class for an outlined method.
boost::optional<size_t> get_class_id(const DexType* t) {
auto it = m_class_ids.find(t);
return it == m_class_ids.end() ? boost::none
: boost::optional<size_t>(it->second);
}
};
// Provides facilities to select existing, or create new host classes for
// outlined methods
class HostClassSelector {
private:
const Config& m_config;
PassManager& m_mgr;
DexState& m_dex_state;
DexClass* m_outlined_cls{nullptr};
size_t m_outlined_classes{0};
size_t m_hosted_direct_count{0};
size_t m_hosted_base_count{0};
size_t m_hosted_at_refs_count{0};
size_t m_hosted_helper_count{0};
int m_min_sdk;
size_t m_iteration;
public:
HostClassSelector() = delete;
HostClassSelector(const HostClassSelector&) = delete;
HostClassSelector& operator=(const HostClassSelector&) = delete;
HostClassSelector(const Config& config,
PassManager& mgr,
DexState& dex_state,
int min_sdk,
size_t iteration)
: m_config(config),
m_mgr(mgr),
m_dex_state(dex_state),
m_min_sdk(min_sdk),
m_iteration(iteration) {}
~HostClassSelector() {
m_mgr.incr_metric("num_hosted_direct_count", m_hosted_direct_count);
m_mgr.incr_metric("num_hosted_base_count", m_hosted_base_count);
m_mgr.incr_metric("num_hosted_at_refs_count", m_hosted_at_refs_count);
m_mgr.incr_metric("num_hosted_helper_count", m_hosted_helper_count);
TRACE(ISO, 2,
"[invoke sequence outliner] %zu direct, %zu base, %zu at refs, %zu "
"helpers hosted",
m_hosted_direct_count, m_hosted_base_count, m_hosted_at_refs_count,
m_hosted_helper_count);
m_mgr.incr_metric("num_outlined_classes", m_outlined_classes);
TRACE(ISO, 2,
"[invoke sequence outliner] %zu outlined helper classes created",
m_outlined_classes);
}
// Return current outlined helper class, if exists and we can add one more
// method to it
DexType* reuse_last_outlined_class() {
if (!m_outlined_cls || m_outlined_cls->get_dmethods().size() >=
m_config.max_outlined_methods_per_class) {
return nullptr;
}
return m_outlined_cls->get_type();
}
// Create type that will represent next outlined helper class
DexType* peek_at_next_outlined_class() {
auto name = DexString::make_string(
std::string(OUTLINED_CLASS_NAME_PREFIX) + std::to_string(m_iteration) +
"$" + std::to_string(m_dex_state.get_dex_id()) + "$" +
std::to_string(m_outlined_classes) + ";");
return DexType::make_type(name);
}
// Create a new helper class into which we can place outlined methods.
void create_next_outlined_class() {
always_assert(reuse_last_outlined_class() == nullptr);
auto outlined_type = peek_at_next_outlined_class();
m_outlined_classes++;
ClassCreator cc(outlined_type);
cc.set_access(ACC_PUBLIC | ACC_FINAL);
cc.set_super(type::java_lang_Object());
m_outlined_cls = cc.create();
m_outlined_cls->rstate.set_generated();
m_outlined_cls->rstate.set_outlined();
m_outlined_cls->set_perf_sensitive(true);
m_dex_state.insert_outlined_class(m_outlined_cls);
}
const DexType* get_direct_or_base_class(
std::unordered_set<const DexType*> types,
const std::function<std::unordered_set<const DexType*>(
const std::unordered_set<const DexType*>&)>& get_super_classes,
const std::function<bool(const DexType*)>& predicate =
[](const DexType*) { return true; }) {
// Let's see if we can reduce the set to a most specific sub-type
if (types.size() > 1) {
for (auto t : get_common_super_classes(types)) {
types.erase(t);
}
}
// When there's only one type, try to use that
if (types.size() == 1) {
auto direct_type = *types.begin();
if (m_dex_state.get_class_id(direct_type) && predicate(direct_type)) {
auto direct_cls = type_class(direct_type);
always_assert(direct_cls);
if (can_rename(direct_cls) && can_delete(direct_cls)) {
return direct_type;
}
}
}
// Try to find the first allowable base types
const DexType* host_class{nullptr};
boost::optional<size_t> host_class_id;
for (auto type : get_super_classes(types)) {
auto class_id = m_dex_state.get_class_id(type);
if (!class_id || !predicate(type)) {
continue;
}
auto cls = type_class(type);
if (!cls || !can_rename(cls) || !can_delete(cls)) {
continue;
}
// In particular, use the base type that appears first in this dex.
if (host_class == nullptr || *host_class_id > *class_id) {
host_class_id = *class_id;
host_class = type;
}
}
return host_class;
}
const DexType* get_direct_or_base_class(const Candidate& c,
const CandidateInfo& ci,
bool* not_outlinable) {
*not_outlinable = false;
// When all candidate instances come from methods of a single class, use
// that type as the host class
auto declaring_types = get_declaring_types(ci);
always_assert(!declaring_types.empty());
auto host_class =
get_direct_or_base_class(declaring_types, get_common_super_classes);
if (declaring_types.size() == 1) {
auto direct_type = *declaring_types.begin();
if (host_class == direct_type) {
m_hosted_direct_count++;
return host_class;
}
if (has_non_init_invoke_directs(c)) {
// TODO: Consider making those methods static if they can be renamed,
// just like what the inliner does
*not_outlinable = true;
return nullptr;
};
}
always_assert(!has_non_init_invoke_directs(c));
if (host_class) {
m_hosted_base_count++;
return host_class;
}
// If an outlined code snippet occurs in many unrelated classes, but always
// references some types that share a common base type, then that common
// base type is a reasonable place where to put the outlined code.
auto referenced_types = get_referenced_types(c);
host_class = get_direct_or_base_class(
referenced_types, get_super_classes,
[min_sdk = m_min_sdk](const DexType* t) {
auto cls = type_class(t);
// Before Android 7, invoking static methods defined in interfaces was
// not supported. See rule A24 in
// https://source.android.com/devices/tech/dalvik/constraints
if (min_sdk < 24 && is_interface(cls)) {
return false;
}
// We don't want any common base class with a scary clinit
return !method::clinit_may_have_side_effects(cls);
});
if (host_class) {
m_hosted_at_refs_count++;
return host_class;
}
// Fallback: put the outlined method in a dedicated helper class.
m_hosted_helper_count++;
return nullptr;
}
};
using NewlyOutlinedMethods =
std::unordered_map<DexMethod*, std::vector<DexMethod*>>;
// Outlining all occurrences of a particular candidate.
bool outline_candidate(
const Config& config,
const Candidate& c,
const CandidateInfo& ci,
ReusableOutlinedMethods* outlined_methods,
NewlyOutlinedMethods* newly_outlined_methods,
DexState* dex_state,
HostClassSelector* host_class_selector,
OutlinedMethodCreator* outlined_method_creator,
std::unique_ptr<ab_test::ABExperimentContext>& ab_experiment_context,
size_t* num_reused_methods,
bool reuse_outlined_methods_across_dexes) {
// Before attempting to create or reuse an outlined method that hasn't been
// referenced in this dex before, we'll make sure that all the involved
// type refs can be added to the dex. We collect those type refs.
std::unordered_set<const DexType*> type_refs_to_insert;
for (auto t : c.arg_types) {
type_refs_to_insert.insert(const_cast<DexType*>(t));
}
auto rtype = c.res_type ? c.res_type : type::_void();
type_refs_to_insert.insert(const_cast<DexType*>(rtype));
DexMethod* outlined_method{find_reusable_method(
c, ci, *outlined_methods, reuse_outlined_methods_across_dexes)};
if (outlined_method) {
type_refs_to_insert.insert(outlined_method->get_class());
if (!dex_state->can_insert_type_refs(type_refs_to_insert)) {
return false;
}
if (config.full_dbg_positions) {
auto& pairs = outlined_methods->map.at(c);
auto it = std::find_if(
pairs.begin(), pairs.end(),
[&outlined_method](
const std::pair<DexMethod*, std::set<uint32_t>>& pair) {
return pair.first == outlined_method;
});
auto& pattern_ids = it->second;
outlined_method_creator->add_outlined_dbg_position_patterns(c, ci,
&pattern_ids);
}
(*num_reused_methods)++;
TRACE(ISO, 5, "[invoke sequence outliner] reused %s",
SHOW(outlined_method));
} else {
bool not_outlinable;
auto host_class =
host_class_selector->get_direct_or_base_class(c, ci, ¬_outlinable);
if (not_outlinable) {
return false;
}
bool must_create_next_outlined_class{false};
if (host_class == nullptr) {
host_class = host_class_selector->reuse_last_outlined_class();
if (host_class == nullptr) {
host_class = host_class_selector->peek_at_next_outlined_class();
must_create_next_outlined_class = true;
}
}
type_refs_to_insert.insert(host_class);
if (!dex_state->can_insert_type_refs(type_refs_to_insert)) {
return false;
}
if (must_create_next_outlined_class) {
host_class_selector->create_next_outlined_class();
}
std::set<uint32_t> position_pattern_ids;
outlined_method = outlined_method_creator->create_outlined_method(
c, ci, host_class, &position_pattern_ids);
outlined_methods->order.push_back(c);
outlined_methods->map[c].push_back({outlined_method, position_pattern_ids});
auto& methods = (*newly_outlined_methods)[outlined_method];
for (auto& p : ci.methods) {
methods.push_back(p.first);
}
}
dex_state->insert_type_refs(type_refs_to_insert);
auto call_site_pattern_ids =
outlined_method_creator->get_call_site_pattern_ids();
for (auto& p : ci.methods) {
auto method = p.first;
auto& cfg = method->get_code()->cfg();
ab_experiment_context->try_register_method(method);
TRACE(ISO, 7, "[invoke sequence outliner] before outlined %s from %s\n%s",
SHOW(outlined_method), SHOW(method), SHOW(cfg));
for (auto& cml : p.second) {
rewrite_at_location(outlined_method, call_site_pattern_ids, method, cfg,
c, cml);
}
TRACE(ISO, 6, "[invoke sequence outliner] after outlined %s from %s\n%s",
SHOW(outlined_method), SHOW(method), SHOW(cfg));
}
return true;
}
// Perform outlining of most beneficial candidates, while staying within
// reference limits.
static NewlyOutlinedMethods outline(
const Config& config,
PassManager& mgr,
DexState& dex_state,
int min_sdk,
std::vector<CandidateWithInfo>* candidates_with_infos,
std::unordered_map<DexMethod*, std::unordered_set<CandidateId>>*
candidate_ids_by_methods,
ReusableOutlinedMethods* outlined_methods,
size_t iteration,
std::unique_ptr<ab_test::ABExperimentContext>& ab_experiment_context,
size_t* num_reused_methods) {
MethodNameGenerator method_name_generator(mgr, iteration);
OutlinedMethodCreator outlined_method_creator(config, mgr,
method_name_generator);
HostClassSelector host_class_selector(config, mgr, dex_state, min_sdk,
iteration);
// While we have a set of beneficial candidates, many are overlapping each
// other. We are using a priority queue to iteratively outline the most
// beneficial candidate at any point in time, then removing all impacted
// other overlapping occurrences, which in turn changes the priority of
// impacted candidates, until there is no more beneficial candidate left.
using Priority = uint64_t;
MutablePriorityQueue<CandidateId, Priority> pq;
auto get_priority = [&config, &candidates_with_infos,
outlined_methods](CandidateId id) {
auto& cwi = candidates_with_infos->at(id);
Priority primary_priority =
get_savings(config, cwi.candidate, cwi.info, *outlined_methods) *
cwi.candidate.size;
// clip primary_priority to 32-bit
if (primary_priority >= (1UL << 32)) {
primary_priority = (1UL << 32) - 1;
}
// make unique via candidate id
return (primary_priority << 32) | id;
};
auto erase = [&pq, candidate_ids_by_methods](CandidateId id,
CandidateWithInfo& cwi) {
pq.erase(id);
for (auto& p : cwi.info.methods) {
(*candidate_ids_by_methods)[p.first].erase(id);
}
cwi.info.methods.clear();
cwi.info.count = 0;
};
for (CandidateId id = 0; id < candidates_with_infos->size(); id++) {
pq.insert(id, get_priority(id));
}
size_t total_savings{0};
size_t outlined_count{0};
size_t outlined_sequences_count{0};
size_t not_outlined_count{0};
NewlyOutlinedMethods newly_outlined_methods;
while (!pq.empty()) {
// Make sure beforehand that there's a method ref left for us
if (!dex_state.can_insert_method_ref()) {
break;
}
auto id = pq.front();
auto& cwi = candidates_with_infos->at(id);
auto savings =
get_savings(config, cwi.candidate, cwi.info, *outlined_methods);
always_assert(savings > 0);
total_savings += savings;
outlined_count += cwi.info.count;
outlined_sequences_count++;
TRACE(ISO, 3,
"[invoke sequence outliner] %4zx(%3zu) [%zu]: %zu byte savings",
cwi.info.count, cwi.info.methods.size(), cwi.candidate.size,
2 * savings);
if (outline_candidate(config, cwi.candidate, cwi.info, outlined_methods,
&newly_outlined_methods, &dex_state,
&host_class_selector, &outlined_method_creator,
ab_experiment_context, num_reused_methods,
config.reuse_outlined_methods_across_dexes)) {
dex_state.insert_method_ref();
} else {
TRACE(ISO, 3, "[invoke sequence outliner] could not ouline");
not_outlined_count++;
}
// Remove overlapping occurrences
std::unordered_set<CandidateId> other_candidate_ids_with_changes;
for (auto& p : cwi.info.methods) {
auto method = p.first;
auto& cmls = p.second;
for (auto other_id : candidate_ids_by_methods->at(method)) {
if (other_id == id) {
continue;
}
auto& other_c = candidates_with_infos->at(other_id);
for (auto& cml : cmls) {
auto& other_cmls = other_c.info.methods.at(method);
std20::erase_if(other_cmls, [&](auto& other_cml) {
if (ranges_overlap(cml.ranges, other_cml.ranges)) {
other_c.info.count--;
if (other_id != id) {
other_candidate_ids_with_changes.insert(other_id);
}
return true;
}
return false;
});
}
}
}
erase(id, cwi);
// Update priorities of affected candidates
for (auto other_id : other_candidate_ids_with_changes) {
auto& other_cwi = candidates_with_infos->at(other_id);
auto other_savings = get_savings(config, other_cwi.candidate,
other_cwi.info, *outlined_methods);
if (other_savings == 0) {
erase(other_id, other_cwi);
} else {
pq.update_priority(other_id, get_priority(other_id));
}
}
}
mgr.incr_metric("num_not_outlined", not_outlined_count);
TRACE(ISO, 2, "[invoke sequence outliner] %zu not outlined",
not_outlined_count);
mgr.incr_metric("num_outlined", outlined_count);
mgr.incr_metric("num_outlined_sequences", outlined_sequences_count);
mgr.incr_metric("num_total_savings", total_savings);
TRACE(ISO, 1,
"[invoke sequence outliner] %zu unique sequences outlined in %zu "
"places; %zu total savings",
outlined_sequences_count, outlined_count, total_savings);
return newly_outlined_methods;
}
size_t count_affected_methods(
const NewlyOutlinedMethods& newly_outlined_methods) {
std::unordered_set<DexMethod*> methods;
for (auto& p : newly_outlined_methods) {
methods.insert(p.second.begin(), p.second.end());
}
return methods.size();
}
////////////////////////////////////////////////////////////////////////////////
// reorder_with_method_profiles
////////////////////////////////////////////////////////////////////////////////
std::unordered_map<const DexMethodRef*, double> get_methods_global_order(
ConfigFiles& config_files, const Config& config) {
if (!config.reorder_with_method_profiles) {
return {};
}
auto& method_profiles = config_files.get_method_profiles();
if (!method_profiles.has_stats()) {
return {};
}
std::unordered_map<std::string, size_t> interaction_indices;
auto register_interaction = [&](const std::string& interaction_id) {
return interaction_indices
.emplace(interaction_id, interaction_indices.size())
.first->second;
};
// Make sure that the "ColdStart" interaction comes before everything else
register_interaction("ColdStart");
std::unordered_map<const DexMethodRef*, double> methods_global_order;
for (auto& p : method_profiles.all_interactions()) {
auto& interaction_id = p.first;
auto& method_stats = p.second;
auto index = register_interaction(interaction_id);
TRACE(ISO, 3,
"[instruction sequence outliner] Interaction [%s] gets index %zu",
interaction_id.c_str(), index);
for (auto& q : method_stats) {
auto& global_order = methods_global_order[q.first];
global_order =
std::min(global_order, index * 100 + q.second.order_percent);
}
}
std::vector<const DexMethodRef*> ordered_methods;
ordered_methods.reserve(methods_global_order.size());
for (auto& p : methods_global_order) {
ordered_methods.push_back(p.first);
}
std::sort(ordered_methods.begin(), ordered_methods.end(),
[&](const DexMethodRef* a, const DexMethodRef* b) {
auto a_order = methods_global_order.at(a);
auto b_order = methods_global_order.at(b);
if (a_order != b_order) {
return a_order < b_order;
}
return compare_dexmethods(a, b);
});
TRACE(ISO, 4, "[instruction sequence outliner] %zu globally ordered methods",
ordered_methods.size());
for (auto method : ordered_methods) {
TRACE(ISO, 5, "[instruction sequence outliner] [%f] %s",
methods_global_order.at(method), SHOW(method));
}
return methods_global_order;
}
void reorder_with_method_profiles(
const Config& config,
PassManager& mgr,
const Scope& dex,
const std::unordered_map<const DexMethodRef*, double>& methods_global_order,
const NewlyOutlinedMethods& newly_outlined_methods) {
if (methods_global_order.empty()) {
return;
}
std::unordered_map<const DexMethod*, double> outlined_methods_global_order;
std::vector<DexMethod*> ordered_outlined_methods;
std::unordered_set<DexClass*> outlined_classes;
for (auto& p : newly_outlined_methods) {
auto cls = type_class(p.first->get_class());
if (!cls->rstate.outlined()) {
continue;
}
outlined_classes.insert(cls);
auto min_order = std::numeric_limits<double>::infinity();
for (auto method : p.second) {
auto it = methods_global_order.find(method);
if (it != methods_global_order.end()) {
min_order = std::min(min_order, it->second);
}
}
outlined_methods_global_order.emplace(p.first, min_order);
ordered_outlined_methods.push_back(p.first);
}
std::vector<DexClass*> ordered_outlined_classes;
for (auto cls : dex) {
if (outlined_classes.count(cls)) {
ordered_outlined_classes.push_back(cls);
TRACE(ISO, 5,
"[instruction sequence outliner] Found outlined class %s with %zu "
"methods",
SHOW(cls), cls->get_dmethods().size());
}
}
std::sort(ordered_outlined_methods.begin(),
ordered_outlined_methods.end(),
[&](const DexMethod* a, const DexMethod* b) {
auto a_order = outlined_methods_global_order.at(a);
auto b_order = outlined_methods_global_order.at(b);
if (a_order != b_order) {
return a_order < b_order;
}
// If order is same, prefer smaller methods, as they are likely
// invoked more often / have higher invocation cost
auto a_size = a->get_code()->sum_opcode_sizes();
auto b_size = b->get_code()->sum_opcode_sizes();
if (a_size != b_size) {
return a_size < b_size;
}
// Then use the method name (which is essentially a stable hash of
// the instructions) as a tie-breaker.
if (a->get_name() != b->get_name()) {
return compare_dexstrings(a->get_name(), b->get_name());
}
// Final tie-breaker is full method signature comparison
return compare_dexmethods(a, b);
});
TRACE(ISO, 4,
"[instruction sequence outliner] %zu globally ordered outlined methods",
ordered_outlined_methods.size());
for (auto method : ordered_outlined_methods) {
TRACE(ISO, 5, "[instruction sequence outliner] [%f] %s",
outlined_methods_global_order.at(method), SHOW(method));
}
size_t class_index = 0;
size_t methods_count = 0;
size_t relocated_outlined_methods = 0;
auto flush = [&]() {
if (methods_count == 0) {
return;
}
auto cls = ordered_outlined_classes.at(class_index);
TRACE(ISO, 4,
"[instruction sequence outliner] Finished outlined class %s with "
"%zu methods",
SHOW(cls), cls->get_dmethods().size());
class_index++;
methods_count = 0;
};
for (auto method : ordered_outlined_methods) {
auto target_class = ordered_outlined_classes.at(class_index);
if (method->get_class() != target_class->get_type()) {
TRACE(ISO, 4,
"[instruction sequence outliner] Relocating outlined method %s "
"from %s to %s",
SHOW(method->get_name()), SHOW(method->get_class()),
SHOW(target_class));
relocate_method(method, target_class->get_type());
method->set_deobfuscated_name(show(method));
relocated_outlined_methods++;
}
if (++methods_count == config.max_outlined_methods_per_class) {
flush();
}
}
flush();
mgr.incr_metric("num_relocated_outlined_methods", relocated_outlined_methods);
}
////////////////////////////////////////////////////////////////////////////////
// clear_cfgs
////////////////////////////////////////////////////////////////////////////////
static size_t clear_cfgs(const Scope& scope) {
std::atomic<size_t> methods{0};
walk::parallel::code(scope, [&methods](DexMethod* method, IRCode& code) {
if (!can_outline_from_method(method)) {
return;
}
code.clear_cfg();
methods++;
});
return (size_t)methods;
}
////////////////////////////////////////////////////////////////////////////////
// reorder_all_methods
////////////////////////////////////////////////////////////////////////////////
// helper function to reorder all the outlined methods
// after all the dex files are processed.
void reorder_all_methods(
const Config& config,
PassManager& mgr,
const std::unordered_map<const DexMethodRef*, double>& methods_global_order,
const std::vector<std::pair<const Scope*, const NewlyOutlinedMethods>>&
outlined_methods_to_reorder) {
for (auto& dex_methods_pair : outlined_methods_to_reorder) {
auto& dex = dex_methods_pair.first;
auto& outlined_methods = dex_methods_pair.second;
reorder_with_method_profiles(config, mgr, *dex, methods_global_order,
outlined_methods);
}
}
class OutlinedMethodBodySetter {
private:
const Config& m_config;
PassManager& m_mgr;
size_t m_outlined_method_body_set{0};
size_t m_outlined_method_nodes{0};
size_t m_outlined_method_positions{0};
size_t m_outlined_method_instructions{0};
public:
OutlinedMethodBodySetter() = delete;
OutlinedMethodBodySetter(const OutlinedMethodBodySetter&) = delete;
OutlinedMethodBodySetter& operator=(const OutlinedMethodBodySetter&) = delete;
explicit OutlinedMethodBodySetter(const Config& config, PassManager& mgr)
: m_config(config), m_mgr(mgr) {}
~OutlinedMethodBodySetter() {
m_mgr.incr_metric("num_outlined_method_body_set",
m_outlined_method_body_set);
m_mgr.incr_metric("num_outlined_method_instructions",
m_outlined_method_instructions);
m_mgr.incr_metric("num_outlined_method_nodes", m_outlined_method_nodes);
m_mgr.incr_metric("num_outlined_method_positions",
m_outlined_method_positions);
TRACE(ISO, 2,
"[invoke sequence outliner] set body for %zu outlined methods with "
"%zu "
"instructions across %zu nodes and %zu positions",
m_outlined_method_body_set, m_outlined_method_instructions,
m_outlined_method_nodes, m_outlined_method_positions);
}
// Construct an IRCode datastructure from a candidate.
std::unique_ptr<IRCode> get_outlined_code(
DexMethod* outlined_method,
const Candidate& c,
const std::set<uint32_t>& current_pattern_ids) {
auto manager = g_redex->get_position_pattern_switch_manager();
std::map<uint32_t, const PositionPattern*> current_patterns;
bool any_positions = false;
const auto& all_managed_patterns = manager->get_patterns();
for (auto pattern_id : current_pattern_ids) {
auto pattern = &all_managed_patterns.at(pattern_id);
current_patterns.emplace(pattern_id, pattern);
if (!pattern->empty()) {
any_positions = true;
}
}
auto code = std::make_unique<IRCode>(outlined_method, c.temp_regs);
if (any_positions) {
code->set_debug_item(std::make_unique<DexDebugItem>());
}
std::function<void(const CandidateNode& cn)> walk;
std::unordered_map<DexPosition*, DexPosition*> cloned_dbg_positions;
std::function<DexPosition*(DexPosition*)> get_or_add_cloned_dbg_position;
get_or_add_cloned_dbg_position =
[this, &code, &get_or_add_cloned_dbg_position,
&cloned_dbg_positions](DexPosition* dbg_pos) -> DexPosition* {
always_assert(dbg_pos);
auto it = cloned_dbg_positions.find(dbg_pos);
if (it != cloned_dbg_positions.end()) {
return it->second;
}
auto cloned_dbg_pos = std::make_unique<DexPosition>(*dbg_pos);
if (dbg_pos->parent) {
cloned_dbg_pos->parent =
get_or_add_cloned_dbg_position(dbg_pos->parent);
}
auto cloned_dbg_pos_ptr = cloned_dbg_pos.get();
code->push_back(std::move(cloned_dbg_pos));
m_outlined_method_positions++;
cloned_dbg_positions.emplace(dbg_pos, cloned_dbg_pos_ptr);
return cloned_dbg_pos_ptr;
};
size_t dbg_positions_idx = 0;
walk = [this, manager, &code, ¤t_patterns, any_positions, &walk, &c,
&get_or_add_cloned_dbg_position,
&dbg_positions_idx](const CandidateNode& cn) {
m_outlined_method_nodes++;
std::unordered_map<uint32_t, DexPosition*> last_dbg_poses;
for (auto& p : current_patterns) {
last_dbg_poses.emplace(p.first, nullptr);
}
for (auto& ci : cn.insns) {
if (!opcode::is_a_move_result_pseudo(ci.core.opcode) && any_positions) {
PositionSwitch position_switch;
bool any_changed = false;
for (auto& p : current_patterns) {
auto pattern_id = p.first;
auto& pattern = p.second;
DexPosition* dbg_pos =
pattern->empty() ? nullptr : pattern->at(dbg_positions_idx);
position_switch.push_back({pattern_id, dbg_pos});
auto& last_dbg_pos = last_dbg_poses.at(pattern_id);
if (dbg_pos != last_dbg_pos) {
always_assert(dbg_pos);
any_changed = true;
last_dbg_pos = dbg_pos;
}
}
if (any_changed) {
if (current_patterns.size() == 1) {
get_or_add_cloned_dbg_position(position_switch.front().position);
} else {
always_assert(position_switch.size() >= 2);
auto switch_id = manager->make_switch(position_switch);
code->push_back(manager->make_switch_position(switch_id));
}
}
}
dbg_positions_idx++;
if (m_config.debug_make_crashing &&
opcode::is_an_iget(ci.core.opcode)) {
auto const_insn = new IRInstruction(OPCODE_CONST);
const_insn->set_literal(0);
const_insn->set_dest(ci.srcs.at(0));
code->push_back(const_insn);
}
auto insn = new IRInstruction(ci.core.opcode);
insn->set_srcs_size(ci.srcs.size());
for (size_t i = 0; i < ci.srcs.size(); i++) {
insn->set_src(i, ci.srcs.at(i));
}
if (ci.dest) {
insn->set_dest(*ci.dest);
}
if (insn->has_method()) {
insn->set_method(ci.core.method);
} else if (insn->has_field()) {
insn->set_field(ci.core.field);
} else if (insn->has_string()) {
insn->set_string(ci.core.string);
} else if (insn->has_type()) {
insn->set_type(ci.core.type);
} else if (insn->has_literal()) {
insn->set_literal(ci.core.literal);
} else if (insn->has_data()) {
insn->set_data(ci.core.data);
}
code->push_back(insn);
}
m_outlined_method_instructions += cn.insns.size();
if (cn.succs.empty()) {
if (c.res_type != nullptr) {
IROpcode ret_opcode =
type::is_object(c.res_type) ? OPCODE_RETURN_OBJECT
: type::is_wide_type(c.res_type) ? OPCODE_RETURN_WIDE
: OPCODE_RETURN;
auto ret_insn = new IRInstruction(ret_opcode);
ret_insn->set_src(0, *cn.res_reg);
code->push_back(ret_insn);
} else {
auto ret_insn = new IRInstruction(OPCODE_RETURN_VOID);
code->push_back(ret_insn);
}
} else {
auto& last_mie = *code->rbegin();
for (auto& p : cn.succs) {
auto& e = p.first;
if (e.type == cfg::EDGE_GOTO) {
always_assert(e == cn.succs.front().first);
code->push_back(); // MFLOW_FALLTHROUGH
} else {
always_assert(e.type == cfg::EDGE_BRANCH);
BranchTarget* branch_target =
e.case_key ? new BranchTarget(&last_mie, *e.case_key)
: new BranchTarget(&last_mie);
code->push_back(branch_target);
}
walk(*p.second);
}
}
};
walk(c.root);
return code;
}
// set body for each method stored in ReusableOutlinedMethod
void set_method_body(ReusableOutlinedMethods& outlined_methods) {
for (auto& c : outlined_methods.order) {
auto& method_pattern_pairs = outlined_methods.map[c];
auto& outlined_method = method_pattern_pairs.front().first;
auto& position_pattern_ids = method_pattern_pairs.front().second;
always_assert(!position_pattern_ids.empty());
outlined_method->set_code(
get_outlined_code(outlined_method, c, position_pattern_ids));
change_visibility(outlined_method->get_code(),
outlined_method->get_class(), outlined_method);
TRACE(ISO, 5, "[invoke sequence outliner] set the body of %s as \n%s",
SHOW(outlined_method), SHOW(outlined_method->get_code()));
m_outlined_method_body_set++;
// pop up the front pair to avoid duplicate
method_pattern_pairs.pop_front();
}
}
};
} // namespace
void InstructionSequenceOutliner::bind_config() {
auto& pg = m_config.profile_guidance;
bind("min_insns_size", m_config.min_insns_size, m_config.min_insns_size,
"Minimum number of instructions to be outlined in a sequence");
bind("max_insns_size", m_config.max_insns_size, m_config.max_insns_size,
"Maximum number of instructions to be outlined in a sequence");
bind("use_method_profiles", pg.use_method_profiles, pg.use_method_profiles,
"Whether to use provided method-profiles configuration data to "
"determine if certain code should not be outlined from a method");
bind("method_profiles_appear_percent",
pg.method_profiles_appear_percent,
pg.method_profiles_appear_percent,
"Cut off when a method in a method profile is deemed relevant");
bind("method_profiles_hot_call_count",
pg.method_profiles_hot_call_count,
pg.method_profiles_hot_call_count,
"No code is outlined out of hot methods");
bind("method_profiles_warm_call_count",
pg.method_profiles_warm_call_count,
pg.method_profiles_warm_call_count,
"Loops are not outlined from warm methods");
std::string perf_sensitivity_str;
bind("perf_sensitivity", "always-hot", perf_sensitivity_str);
bind("block_profiles_hits",
pg.block_profiles_hits,
pg.block_profiles_hits,
"No code is outlined out of hot blocks in hot methods");
bind("reuse_outlined_methods_across_dexes",
m_config.reuse_outlined_methods_across_dexes,
m_config.reuse_outlined_methods_across_dexes,
"Whether to allow reusing outlined methods across dexes within the same "
"store");
bind("max_outlined_methods_per_class",
m_config.max_outlined_methods_per_class,
m_config.max_outlined_methods_per_class,
"Maximum number of outlined methods per generated helper class; "
"indirectly drives number of needed helper classes");
bind("savings_threshold", m_config.savings_threshold,
m_config.savings_threshold,
"Minimum number of code units saved before a particular code sequence "
"is outlined anywhere");
bind("outline_from_primary_dex", m_config.outline_from_primary_dex,
m_config.outline_from_primary_dex,
"Whether to outline from primary dex");
bind("full_dbg_positions", m_config.full_dbg_positions,
m_config.full_dbg_positions,
"Whether to encode all possible outlined positions");
bind("debug_make_crashing", m_config.debug_make_crashing,
m_config.debug_make_crashing,
"Make outlined code crash, to harvest crashing stack traces involving "
"outlined code.");
after_configuration([=]() {
always_assert(m_config.min_insns_size >= MIN_INSNS_SIZE);
always_assert(m_config.max_insns_size >= m_config.min_insns_size);
always_assert(m_config.max_outlined_methods_per_class > 0);
always_assert(!perf_sensitivity_str.empty());
m_config.profile_guidance.perf_sensitivity =
parse_perf_sensitivity(perf_sensitivity_str);
});
}
void InstructionSequenceOutliner::run_pass(DexStoresVector& stores,
ConfigFiles& config,
PassManager& mgr) {
auto ab_experiment_context =
ab_test::ABExperimentContext::create("outliner_v1");
if (ab_experiment_context->use_control()) {
return;
}
int32_t min_sdk = mgr.get_redex_options().min_sdk;
mgr.incr_metric("min_sdk", min_sdk);
TRACE(ISO, 2, "[invoke sequence outliner] min_sdk: %d", min_sdk);
auto min_sdk_api_file = config.get_android_sdk_api_file(min_sdk);
const api::AndroidSDK* min_sdk_api{nullptr};
if (!min_sdk_api_file) {
mgr.incr_metric("min_sdk_no_file", 1);
TRACE(ISO, 2,
"[invoke sequence outliner] Android SDK API %d file cannot be found.",
min_sdk);
} else {
min_sdk_api = &config.get_android_sdk_api(min_sdk);
}
auto scope = build_class_scope(stores);
init_classes::InitClassesWithSideEffects init_classes_with_side_effects(
scope, config.create_init_class_insns());
std::unordered_set<DexMethod*> sufficiently_warm_methods;
std::unordered_set<DexMethod*> sufficiently_hot_methods;
gather_sufficiently_warm_and_hot_methods(
scope, config, mgr, m_config.profile_guidance, &sufficiently_warm_methods,
&sufficiently_hot_methods);
mgr.incr_metric("num_sufficiently_warm_methods",
sufficiently_warm_methods.size());
mgr.incr_metric("num_sufficiently_hot_methods",
sufficiently_hot_methods.size());
auto methods_global_order = get_methods_global_order(config, m_config);
mgr.incr_metric("num_ordered_methods", methods_global_order.size());
XStoreRefs xstores(stores);
size_t dex_id{0};
const auto& interdex_metrics = mgr.get_interdex_metrics();
auto it = interdex_metrics.find(interdex::METRIC_RESERVED_MREFS);
size_t reserved_mrefs = it == interdex_metrics.end() ? 0 : it->second;
it = interdex_metrics.find(interdex::METRIC_RESERVED_TREFS);
size_t reserved_trefs = it == interdex_metrics.end() ? 0 : it->second;
TRACE(
ISO, 2,
"[invoke sequence outliner] found %zu reserved trefs, %zu reserved mrefs",
reserved_trefs, reserved_mrefs);
ReusableOutlinedMethods outlined_methods;
OutlinedMethodBodySetter outlined_method_body_setter(m_config, mgr);
// keep track of the outlined methods and scope for reordering later
std::vector<std::pair<const Scope*, const NewlyOutlinedMethods>>
outlined_methods_to_reorder;
size_t num_reused_methods{0};
boost::optional<size_t> last_store_idx;
auto iteration = m_iteration++;
bool is_primary_dex{true};
for (auto& store : stores) {
for (auto& dex : store.get_dexen()) {
if (is_primary_dex) {
is_primary_dex = false;
if (!m_config.outline_from_primary_dex) {
// Don't touch the primary dex
continue;
}
}
if (dex.empty()) {
continue;
}
auto store_idx = xstores.get_store_idx(dex.front()->get_type());
always_assert(std::find_if(dex.begin(), dex.end(),
[&xstores, store_idx](DexClass* cls) {
return xstores.get_store_idx(
cls->get_type()) != store_idx;
}) == dex.end());
if (last_store_idx &&
xstores.illegal_ref_between_stores(store_idx, *last_store_idx)) {
// TODO: Keep around all store dependencies and reuse when possible
// set method body before the storage is cleared.
outlined_method_body_setter.set_method_body(outlined_methods);
TRACE(ISO, 3,
"Clearing reusable outlined methods when transitioning from "
"store %zu to %zu",
*last_store_idx, store_idx);
outlined_methods.map.clear();
outlined_methods.order.clear();
}
last_store_idx = store_idx;
RefChecker ref_checker{&xstores, store_idx, min_sdk_api};
CandidateInstructionCoresSet recurring_cores;
ConcurrentMap<DexMethod*, CanOutlineBlockDecider> block_deciders;
get_recurring_cores(m_config, mgr, dex, sufficiently_warm_methods,
sufficiently_hot_methods, ref_checker,
&recurring_cores, &block_deciders);
std::vector<CandidateWithInfo> candidates_with_infos;
std::unordered_map<DexMethod*, std::unordered_set<CandidateId>>
candidate_ids_by_methods;
get_beneficial_candidates(
m_config, mgr, dex, ref_checker, recurring_cores, block_deciders,
&outlined_methods, &candidates_with_infos, &candidate_ids_by_methods);
// TODO: Merge candidates that are equivalent except that one returns
// something and the other doesn't. Affects around 1.5% of candidates.
DexState dex_state(mgr, init_classes_with_side_effects, dex, dex_id++,
reserved_trefs, reserved_mrefs);
auto newly_outlined_methods =
outline(m_config, mgr, dex_state, min_sdk, &candidates_with_infos,
&candidate_ids_by_methods, &outlined_methods, iteration,
ab_experiment_context, &num_reused_methods);
outlined_methods_to_reorder.push_back({&dex, newly_outlined_methods});
auto affected_methods = count_affected_methods(newly_outlined_methods);
auto total_methods = clear_cfgs(dex);
if (total_methods > 0) {
mgr.incr_metric(std::string("percent_methods_affected_in_Dex") +
std::to_string(dex_id),
affected_methods * 100 / total_methods);
}
}
}
// set body of the methods after all
// patterns are discovered then reorder
outlined_method_body_setter.set_method_body(outlined_methods);
reorder_all_methods(m_config, mgr, methods_global_order,
outlined_methods_to_reorder);
ab_experiment_context->flush();
mgr.incr_metric("num_reused_methods", num_reused_methods);
}
static InstructionSequenceOutliner s_pass;