service/constant-propagation/ConstantPropagationTransform.cpp (954 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.
*/
#include "ConstantPropagationTransform.h"
#include "ReachingDefinitions.h"
#include "RedexContext.h"
#include "ScopedMetrics.h"
#include "SignedConstantDomain.h"
#include "StlUtil.h"
#include "Trace.h"
#include "Transform.h"
#include "TypeInference.h"
namespace constant_propagation {
/*
* Replace an instruction that has a single destination register with a `const`
* load. `env` holds the state of the registers after `insn` has been
* evaluated. So, `env.get(dest)` holds the _new_ value of the destination
* register.
*/
bool Transform::replace_with_const(const ConstantEnvironment& env,
const cfg::InstructionIterator& cfg_it,
const XStoreRefs* xstores,
const DexType* declaring_type) {
auto* insn = cfg_it->insn;
auto value = env.get(insn->dest());
auto replacement = ConstantValue::apply_visitor(
value_to_instruction_visitor(insn, xstores, declaring_type), value);
if (replacement.empty()) {
return false;
}
if (opcode::is_a_move_result_pseudo(insn->opcode())) {
auto primary_it = cfg_it.cfg().primary_instruction_of_move_result(cfg_it);
m_mutation->replace(primary_it, replacement);
} else {
m_mutation->replace(cfg_it, replacement);
}
++m_stats.materialized_consts;
return true;
}
/*
* Add an const after load param section for a known value load_param.
* This will depend on future run of RemoveUnusedArgs pass to get the win of
* removing not used arguments.
*/
void Transform::generate_const_param(const ConstantEnvironment& env,
const cfg::InstructionIterator& cfg_it,
const XStoreRefs* xstores,
const DexType* declaring_type) {
auto* insn = cfg_it->insn;
auto value = env.get(insn->dest());
auto replacement = ConstantValue::apply_visitor(
value_to_instruction_visitor(insn, xstores, declaring_type), value);
if (replacement.empty()) {
return;
}
m_added_param_values.insert(m_added_param_values.end(), replacement.begin(),
replacement.end());
++m_stats.added_param_const;
}
bool Transform::eliminate_redundant_null_check(
const ConstantEnvironment& env,
const WholeProgramState& /* unused */,
const cfg::InstructionIterator& cfg_it) {
auto* insn = cfg_it->insn;
switch (insn->opcode()) {
case OPCODE_INVOKE_STATIC: {
if (auto index =
get_null_check_object_index(insn, m_kotlin_null_check_assertions)) {
++m_stats.null_checks_method_calls;
auto val = env.get(insn->src(*index)).maybe_get<SignedConstantDomain>();
if (val && val->interval() == sign_domain::Interval::NEZ) {
m_mutation->remove(cfg_it);
++m_stats.null_checks;
return true;
}
}
break;
}
default:
break;
}
return false;
}
bool Transform::eliminate_redundant_put(
const ConstantEnvironment& env,
const WholeProgramState& wps,
const cfg::InstructionIterator& cfg_it) {
auto* insn = cfg_it->insn;
switch (insn->opcode()) {
case OPCODE_SPUT:
case OPCODE_SPUT_BOOLEAN:
case OPCODE_SPUT_BYTE:
case OPCODE_SPUT_CHAR:
case OPCODE_SPUT_OBJECT:
case OPCODE_SPUT_SHORT:
case OPCODE_SPUT_WIDE:
case OPCODE_IPUT:
case OPCODE_IPUT_BOOLEAN:
case OPCODE_IPUT_BYTE:
case OPCODE_IPUT_CHAR:
case OPCODE_IPUT_OBJECT:
case OPCODE_IPUT_SHORT:
case OPCODE_IPUT_WIDE: {
auto* field = resolve_field(insn->get_field());
if (!field) {
break;
}
// WholeProgramState tells us the observable abstract value of a field
// across all program traces outside their class's <clinit> or <init>, so we
// need to join with 0 here as we are effectively creating a new observation
// point at which the field might still have its default value.
// The ConstantEnvironment tells us the abstract value of a non-escaping
// field at this particular program point.
ConstantValue existing_val;
if (m_config.class_under_init == field->get_class()) {
existing_val = env.get(field);
} else {
existing_val = wps.get_field_value(field);
existing_val.join_with(SignedConstantDomain(0));
}
auto new_val = env.get(insn->src(0));
if (ConstantValue::apply_visitor(runtime_equals_visitor(), existing_val,
new_val)) {
TRACE(FINALINLINE, 2, "%s has %s", SHOW(field), SHOW(existing_val));
// This field must already hold this value. We don't need to write to it
// again.
m_mutation->remove(cfg_it);
++m_stats.redundant_puts_removed;
return true;
}
break;
}
default: {
break;
}
}
return false;
}
namespace {
void try_simplify(const ConstantEnvironment& env,
const cfg::InstructionIterator& cfg_it,
const Transform::Config& config,
cfg::CFGMutation& mutation) {
auto* insn = cfg_it->insn;
auto reg_is_exact = [&env](reg_t reg, int64_t val) {
auto value = env.get(reg).maybe_get<SignedConstantDomain>();
if (!value || !value->get_constant() || *value->get_constant() != val) {
return false;
}
return true;
};
auto reg_fits_lit8 = [&env](reg_t reg) -> std::optional<int8_t> {
auto value = env.get(reg).maybe_get<SignedConstantDomain>();
if (!value || !value->get_constant()) {
return std::nullopt;
}
int64_t val = *value->get_constant();
if (val < -128 || val > 127) {
return std::nullopt;
}
return (int8_t)val;
};
auto maybe_reduce_lit8 = [&](size_t idx) -> bool {
if (!config.to_int_lit8) {
return false;
}
auto val = reg_fits_lit8(insn->src(idx));
if (!val) {
return false;
}
auto new_op = [&]() -> IROpcode {
switch (insn->opcode()) {
case OPCODE_ADD_INT:
return OPCODE_ADD_INT_LIT8;
// TODO: SUB to RSUB
case OPCODE_MUL_INT:
return OPCODE_MUL_INT_LIT8;
case OPCODE_AND_INT:
return OPCODE_AND_INT_LIT8;
case OPCODE_OR_INT:
return OPCODE_OR_INT_LIT8;
case OPCODE_XOR_INT:
return OPCODE_XOR_INT_LIT8;
default:
always_assert(false);
}
not_reached();
}();
auto repl = new IRInstruction(new_op);
repl->set_src(0, insn->src(idx == 0 ? 1 : 0));
repl->set_dest(insn->dest());
repl->set_literal(*val);
mutation.replace(cfg_it, {repl});
return true;
};
auto maybe_reduce_lit8_both = [&]() {
if (maybe_reduce_lit8(0)) {
return true;
}
if (maybe_reduce_lit8(1)) {
return true;
}
return false;
};
auto reg_fits_lit16 = [&env](reg_t reg) -> std::optional<int16_t> {
auto value = env.get(reg).maybe_get<SignedConstantDomain>();
if (!value || !value->get_constant()) {
return std::nullopt;
}
int64_t val = *value->get_constant();
if (val < -32768 || val > 32767) {
return std::nullopt;
}
return (int16_t)val;
};
auto maybe_reduce_lit16 = [&](size_t idx) -> bool {
if (!config.to_int_lit16) {
return false;
}
auto val = reg_fits_lit16(insn->src(idx));
if (!val) {
return false;
}
auto new_op = [&]() -> IROpcode {
switch (insn->opcode()) {
case OPCODE_ADD_INT:
return OPCODE_ADD_INT_LIT16;
// TODO: SUB to RSUB
case OPCODE_MUL_INT:
return OPCODE_MUL_INT_LIT16;
case OPCODE_AND_INT:
return OPCODE_AND_INT_LIT16;
case OPCODE_OR_INT:
return OPCODE_OR_INT_LIT16;
case OPCODE_XOR_INT:
return OPCODE_XOR_INT_LIT16;
default:
always_assert(false);
}
not_reached();
}();
auto repl = new IRInstruction(new_op);
repl->set_src(0, insn->src(idx == 0 ? 1 : 0));
repl->set_dest(insn->dest());
repl->set_literal(*val);
mutation.replace(cfg_it, {repl});
return true;
};
auto maybe_reduce_lit16_both = [&]() {
if (maybe_reduce_lit16(0)) {
return true;
}
if (maybe_reduce_lit16(1)) {
return true;
}
return false;
};
auto replace_with_move = [&](reg_t src_reg) {
auto* move = new IRInstruction(OPCODE_MOVE);
move->set_src(0, src_reg);
move->set_dest(insn->dest());
mutation.replace(cfg_it, {move});
};
auto replace_with_const = [&](int64_t val) {
auto* c = new IRInstruction(OPCODE_CONST);
c->set_dest(insn->dest());
c->set_literal(val);
mutation.replace(cfg_it, {c});
};
auto replace_with_neg = [&](reg_t src_reg) {
auto* neg = new IRInstruction(OPCODE_NEG_INT);
neg->set_src(0, src_reg);
neg->set_dest(insn->dest());
mutation.replace(cfg_it, {neg});
};
switch (insn->opcode()) {
// These should have been handled by PeepHole, really.
case OPCODE_ADD_INT_LIT16:
case OPCODE_ADD_INT_LIT8: {
if (insn->get_literal() == 0) {
replace_with_move(insn->src(0));
}
break;
}
case OPCODE_RSUB_INT:
case OPCODE_RSUB_INT_LIT8: {
if (insn->get_literal() == 0) {
replace_with_neg(insn->src(0));
}
break;
}
case OPCODE_MUL_INT_LIT16:
case OPCODE_MUL_INT_LIT8: {
if (insn->get_literal() == 1) {
replace_with_move(insn->src(0));
break;
}
if (insn->get_literal() == 0) {
replace_with_const(0);
break;
}
if (insn->get_literal() == -1) {
replace_with_neg(insn->src(0));
break;
}
break;
}
case OPCODE_AND_INT_LIT16:
case OPCODE_AND_INT_LIT8: {
if (insn->get_literal() == 0) {
replace_with_const(0);
break;
}
if (insn->get_literal() == -1) {
replace_with_move(insn->src(0));
break;
}
break;
}
case OPCODE_OR_INT_LIT16:
case OPCODE_OR_INT_LIT8: {
if (insn->get_literal() == 0) {
replace_with_move(insn->src(0));
break;
}
if (insn->get_literal() == -1) {
replace_with_const(-1);
break;
}
break;
}
case OPCODE_XOR_INT_LIT16:
case OPCODE_XOR_INT_LIT8: {
// TODO
break;
}
case OPCODE_SHL_INT_LIT8:
case OPCODE_USHR_INT_LIT8:
case OPCODE_SHR_INT_LIT8: {
// Can at most simplify the operand, but doesn't make much sense.
break;
}
case OPCODE_ADD_INT: {
if (reg_is_exact(insn->src(0), 0)) {
replace_with_move(insn->src(1));
} else if (reg_is_exact(insn->src(1), 0)) {
replace_with_move(insn->src(0));
} else if (maybe_reduce_lit8_both()) {
break;
} else if (maybe_reduce_lit16_both()) {
break;
}
break;
}
case OPCODE_SUB_INT: {
if (reg_is_exact(insn->src(0), 0)) {
replace_with_neg(insn->src(1));
} else if (reg_is_exact(insn->src(1), 0)) {
replace_with_move(insn->src(0));
}
break;
}
case OPCODE_MUL_INT: {
if (reg_is_exact(insn->src(0), 1)) {
replace_with_move(insn->src(1));
} else if (reg_is_exact(insn->src(1), 1)) {
replace_with_move(insn->src(0));
} else if (reg_is_exact(insn->src(0), 0) || reg_is_exact(insn->src(1), 0)) {
replace_with_const(0);
} else if (reg_is_exact(insn->src(0), -1)) {
replace_with_neg(insn->src(1));
} else if (reg_is_exact(insn->src(1), -1)) {
replace_with_neg(insn->src(0));
} else if (maybe_reduce_lit8_both()) {
break;
} else if (maybe_reduce_lit16_both()) {
break;
}
break;
}
case OPCODE_AND_INT: {
if (reg_is_exact(insn->src(0), -1)) {
replace_with_move(insn->src(1));
} else if (reg_is_exact(insn->src(1), -1)) {
replace_with_move(insn->src(0));
} else if (reg_is_exact(insn->src(0), 0) || reg_is_exact(insn->src(1), 0)) {
replace_with_const(0);
} else if (maybe_reduce_lit8_both()) {
break;
} else if (maybe_reduce_lit16_both()) {
break;
}
break;
}
case OPCODE_OR_INT: {
if (reg_is_exact(insn->src(0), 0)) {
replace_with_move(insn->src(1));
} else if (reg_is_exact(insn->src(1), 0)) {
replace_with_move(insn->src(0));
} else if (reg_is_exact(insn->src(0), -1) ||
reg_is_exact(insn->src(1), -1)) {
replace_with_const(-1);
} else if (maybe_reduce_lit8_both()) {
break;
} else if (maybe_reduce_lit16_both()) {
break;
}
break;
}
case OPCODE_XOR_INT:
if (maybe_reduce_lit8_both()) {
break;
} else if (maybe_reduce_lit16_both()) {
break;
}
break;
case OPCODE_ADD_LONG:
case OPCODE_SUB_LONG:
case OPCODE_MUL_LONG:
case OPCODE_AND_LONG:
case OPCODE_OR_LONG:
case OPCODE_XOR_LONG:
// TODO: More complicated version of the above.
break;
default:
return;
}
}
} // namespace
void Transform::simplify_instruction(const ConstantEnvironment& env,
const WholeProgramState& wps,
const cfg::InstructionIterator& cfg_it,
const XStoreRefs* xstores,
const DexType* declaring_type) {
auto* insn = cfg_it->insn;
switch (insn->opcode()) {
case IOPCODE_LOAD_PARAM:
case IOPCODE_LOAD_PARAM_OBJECT:
case IOPCODE_LOAD_PARAM_WIDE: {
if (m_config.add_param_const) {
generate_const_param(env, cfg_it, xstores, declaring_type);
}
break;
}
case OPCODE_MOVE:
case OPCODE_MOVE_WIDE:
if (m_config.replace_moves_with_consts) {
replace_with_const(env, cfg_it, xstores, declaring_type);
}
break;
case IOPCODE_MOVE_RESULT_PSEUDO:
case IOPCODE_MOVE_RESULT_PSEUDO_WIDE:
case IOPCODE_MOVE_RESULT_PSEUDO_OBJECT: {
auto& cfg = cfg_it.cfg();
auto primary_insn = cfg.primary_instruction_of_move_result(cfg_it)->insn;
auto op = primary_insn->opcode();
if (opcode::is_an_sget(op) || opcode::is_an_iget(op) ||
opcode::is_an_aget(op) || opcode::is_div_int_lit(op) ||
opcode::is_rem_int_lit(op) || opcode::is_instance_of(op) ||
opcode::is_rem_int_or_long(op) || opcode::is_div_int_or_long(op) ||
opcode::is_check_cast(op)) {
replace_with_const(env, cfg_it, xstores, declaring_type);
}
break;
}
// Currently it's default to not replace move-result opcodes with consts
// because it's unlikely that we can get a more compact encoding (move-result
// can address 8-bit register operands while taking up just 1 code unit).
// However it can be a net win if we can remove the invoke opcodes as well --
// we need a purity analysis for that though.
case OPCODE_MOVE_RESULT:
case OPCODE_MOVE_RESULT_WIDE:
case OPCODE_MOVE_RESULT_OBJECT: {
if (m_config.replace_move_result_with_consts) {
replace_with_const(env, cfg_it, xstores, declaring_type);
} else if (m_config.getter_methods_for_immutable_fields) {
auto& cfg = cfg_it.cfg();
auto primary_insn = cfg.primary_instruction_of_move_result(cfg_it)->insn;
if (opcode::is_invoke_virtual(primary_insn->opcode())) {
auto invoked =
resolve_method(primary_insn->get_method(), MethodSearch::Virtual);
if (m_config.getter_methods_for_immutable_fields->count(invoked)) {
replace_with_const(env, cfg_it, xstores, declaring_type);
}
}
}
break;
}
case OPCODE_ADD_INT_LIT16:
case OPCODE_ADD_INT_LIT8:
case OPCODE_RSUB_INT:
case OPCODE_RSUB_INT_LIT8:
case OPCODE_MUL_INT_LIT16:
case OPCODE_MUL_INT_LIT8:
case OPCODE_AND_INT_LIT16:
case OPCODE_AND_INT_LIT8:
case OPCODE_OR_INT_LIT16:
case OPCODE_OR_INT_LIT8:
case OPCODE_XOR_INT_LIT16:
case OPCODE_XOR_INT_LIT8:
case OPCODE_SHL_INT_LIT8:
case OPCODE_SHR_INT_LIT8:
case OPCODE_USHR_INT_LIT8:
case OPCODE_ADD_INT:
case OPCODE_SUB_INT:
case OPCODE_MUL_INT:
case OPCODE_AND_INT:
case OPCODE_OR_INT:
case OPCODE_XOR_INT:
case OPCODE_ADD_LONG:
case OPCODE_SUB_LONG:
case OPCODE_MUL_LONG:
case OPCODE_AND_LONG:
case OPCODE_OR_LONG:
case OPCODE_XOR_LONG: {
if (replace_with_const(env, cfg_it, xstores, declaring_type)) {
break;
}
try_simplify(env, cfg_it, m_config, *m_mutation);
break;
}
default: {
}
}
}
void Transform::remove_dead_switch(
const intraprocedural::FixpointIterator& intra_cp,
const ConstantEnvironment& env,
cfg::ControlFlowGraph& cfg,
cfg::Block* block) {
if (!m_config.remove_dead_switch) {
return;
}
auto insn_it = block->get_last_insn();
always_assert(insn_it != block->end());
auto* insn = insn_it->insn;
always_assert(opcode::is_switch(insn->opcode()));
// Prune infeasible or unnecessary branches
cfg::Edge* goto_edge = cfg.get_succ_edge_of_type(block, cfg::EDGE_GOTO);
cfg::Block* goto_target = goto_edge->target();
std::unordered_map<cfg::Block*, uint32_t> remaining_branch_targets;
std::vector<cfg::Edge*> remaining_branch_edges;
for (auto branch_edge : cfg.get_succ_edges_of_type(block, cfg::EDGE_BRANCH)) {
auto branch_is_feasible =
!intra_cp.analyze_edge(branch_edge, env).is_bottom();
if (branch_is_feasible) {
remaining_branch_edges.push_back(branch_edge);
remaining_branch_targets[branch_edge->target()]++;
continue;
}
m_edge_deletes.push_back(branch_edge);
}
bool goto_is_feasible = !intra_cp.analyze_edge(goto_edge, env).is_bottom();
if (!goto_is_feasible && !remaining_branch_targets.empty()) {
// Rewire infeasible goto to absorb all cases to most common target
boost::optional<int32_t> most_common_case_key;
cfg::Block* most_common_target{nullptr};
uint32_t most_common_target_count{0};
std::unordered_set<cfg::Block*> visited;
for (cfg::Edge* e : remaining_branch_edges) {
auto case_key = *e->case_key();
auto target = e->target();
auto count = remaining_branch_targets.at(target);
always_assert(count > 0);
if (count > most_common_target_count ||
(count == most_common_target_count &&
case_key > *most_common_case_key)) {
most_common_case_key = case_key;
most_common_target = target;
most_common_target_count = count;
}
}
always_assert(most_common_target != nullptr);
if (most_common_target != goto_target) {
m_edge_deletes.push_back(goto_edge);
goto_target = most_common_target;
m_edge_adds.emplace_back(block, goto_target, cfg::EDGE_GOTO);
goto_edge = nullptr;
}
auto removed = std20::erase_if(remaining_branch_edges, [&](auto* e) {
if (e->target() == most_common_target) {
m_edge_deletes.push_back(e);
return true;
}
return false;
});
always_assert(removed == most_common_target_count);
remaining_branch_targets.erase(most_common_target);
++m_stats.branches_removed;
// goto is now feasible
}
// When all remaining branches are infeasible, the cfg will remove the switch
// instruction.
if (remaining_branch_targets.empty()) {
++m_stats.branches_removed;
return;
}
always_assert(!remaining_branch_edges.empty());
remaining_branch_targets[goto_target]++;
if (remaining_branch_targets.size() > 1) {
return;
}
always_assert(remaining_branch_targets.size() == 1);
++m_stats.branches_removed;
// Replace the switch by a goto to the uniquely reachable block
// We do that by deleting all but one of the remaining branch edges, and then
// the cfg will rewrite the remaining branch into a goto and remove the switch
// instruction.
m_edge_deletes.insert(m_edge_deletes.end(),
remaining_branch_edges.begin(),
remaining_branch_edges.end());
}
/*
* If the last instruction in a basic block is an if-* instruction, determine
* whether it is dead (i.e. whether the branch always taken or never taken).
* If it is, we can replace it with either a nop or a goto.
*/
void Transform::eliminate_dead_branch(
const intraprocedural::FixpointIterator& intra_cp,
const ConstantEnvironment& env,
cfg::ControlFlowGraph& cfg,
cfg::Block* block) {
auto insn_it = block->get_last_insn();
if (insn_it == block->end()) {
return;
}
auto* insn = insn_it->insn;
if (opcode::is_switch(insn->opcode())) {
remove_dead_switch(intra_cp, env, cfg, block);
return;
}
if (!opcode::is_a_conditional_branch(insn->opcode())) {
return;
}
// Get all normal succs (goto/branch edges, excluding ghost edges).
const auto succs = cfg.get_succ_edges_if(block, [](const auto* e) {
return e->type() == cfg::EDGE_GOTO || e->type() == cfg::EDGE_BRANCH;
});
always_assert_log(succs.size() == 2, "actually %zu\n%s in B%zu:\n%s",
succs.size(), SHOW(InstructionIterable(*block)),
block->id(), SHOW(cfg));
for (auto& edge : succs) {
// Check if the fixpoint analysis has determined the successors to be
// unreachable
if (intra_cp.analyze_edge(edge, env).is_bottom()) {
TRACE(CONSTP, 2, "Removing conditional branch %s", SHOW(insn));
++m_stats.branches_removed;
// We delete the infeasible edge, and then the cfg will rewrite the
// remaining branch into a goto and remove the if- instruction.
m_edge_deletes.push_back(edge);
// Assuming :block is reachable, then at least one of its successors must
// be reachable, so we can break after finding one that's unreachable
break;
}
}
}
bool Transform::replace_with_throw(
const ConstantEnvironment& env,
const cfg::InstructionIterator& cfg_it,
npe::NullPointerExceptionCreator* npe_creator) {
auto* insn = cfg_it->insn;
auto dereferenced_object_src_index = get_dereferenced_object_src_index(insn);
if (!dereferenced_object_src_index) {
return false;
}
auto reg = insn->src(*dereferenced_object_src_index);
auto value = env.get(reg).maybe_get<SignedConstantDomain>();
std::vector<IRInstruction*> new_insns;
if (!value || !value->get_constant() || *value->get_constant() != 0) {
return false;
}
// We'll replace this instruction with a different instruction sequence that
// unconditionally throws a null pointer exception.
m_mutation->replace(cfg_it, npe_creator->get_insns(insn));
++m_stats.throws;
if (insn->has_move_result_any()) {
auto& cfg = cfg_it.cfg();
auto move_result_it = cfg.move_result_of(cfg_it);
if (!move_result_it.is_end()) {
m_redundant_move_results.insert(move_result_it->insn);
}
}
return true;
}
void Transform::apply_changes(cfg::ControlFlowGraph& cfg) {
if (!m_edge_adds.empty()) {
for (auto& t : m_edge_adds) {
cfg.add_edge(std::get<0>(t), std::get<1>(t), std::get<2>(t));
}
m_edge_adds.clear();
}
if (!m_edge_deletes.empty()) {
cfg.delete_edges(m_edge_deletes.begin(), m_edge_deletes.end());
m_edge_deletes.clear();
}
always_assert(m_mutation != nullptr);
m_mutation->flush();
if (!m_added_param_values.empty()) {
// Insert after last load-param (and not before first non-load-param
// instructions, as that may suggest that the added instructions are to be
// associated with the position of the non-load-param instruction).
auto block = cfg.entry_block();
auto last_load_params_it = block->get_last_param_loading_insn();
if (last_load_params_it == block->end()) {
block->push_front(m_added_param_values);
} else {
cfg.insert_after(block->to_cfg_instruction_iterator(last_load_params_it),
m_added_param_values);
}
m_added_param_values.clear();
}
}
void Transform::apply(const intraprocedural::FixpointIterator& fp_iter,
const WholeProgramState& wps,
cfg::ControlFlowGraph& cfg,
const XStoreRefs* xstores,
bool is_static,
DexType* declaring_type,
DexProto* proto) {
legacy_apply_constants_and_prune_unreachable(fp_iter, wps, cfg, xstores,
declaring_type);
if (xstores && !g_redex->instrument_mode) {
m_stats.unreachable_instructions_removed += cfg.simplify();
fp_iter.clear_switch_succ_cache();
// legacy_apply_constants_and_prune_unreachable creates some new blocks that
// fp_iter isn't aware of. As turns out, legacy_apply_forward_targets
// doesn't care, and will still do the right thing.
legacy_apply_forward_targets(fp_iter, cfg, is_static, declaring_type, proto,
xstores);
m_stats.unreachable_instructions_removed +=
cfg.remove_unreachable_blocks().first;
}
}
void Transform::legacy_apply_constants_and_prune_unreachable(
const intraprocedural::FixpointIterator& intra_cp,
const WholeProgramState& wps,
cfg::ControlFlowGraph& cfg,
const XStoreRefs* xstores,
const DexType* declaring_type) {
always_assert(cfg.editable());
always_assert(m_mutation == nullptr);
m_mutation = std::make_unique<cfg::CFGMutation>(cfg);
npe::NullPointerExceptionCreator npe_creator(&cfg);
for (const auto& block : cfg.blocks()) {
auto env = intra_cp.get_entry_state_at(block);
// This block is unreachable, no point mutating its instructions -- DCE
// will be removing it anyway
if (env.is_bottom()) {
continue;
}
auto last_insn = block->get_last_insn();
auto ii = InstructionIterable(block);
for (auto it = ii.begin(); it != ii.end(); it++) {
auto cfg_it = block->to_cfg_instruction_iterator(it);
bool any_changes = eliminate_redundant_put(env, wps, cfg_it) ||
eliminate_redundant_null_check(env, wps, cfg_it) ||
replace_with_throw(env, cfg_it, &npe_creator);
auto* insn = cfg_it->insn;
intra_cp.analyze_instruction(insn, &env, insn == last_insn->insn);
if (!any_changes && !m_redundant_move_results.count(insn)) {
simplify_instruction(env, wps, cfg_it, xstores, declaring_type);
}
}
eliminate_dead_branch(intra_cp, env, cfg, block);
}
apply_changes(cfg);
m_mutation = nullptr;
cfg.simplify();
}
void Transform::forward_targets(
const intraprocedural::FixpointIterator& intra_cp,
const ConstantEnvironment& env,
cfg::ControlFlowGraph& cfg,
cfg::Block* block,
std::unique_ptr<LivenessFixpointIterator>& liveness_fixpoint_iter) {
always_assert(!env.is_bottom());
// normal edges are of type goto or branch, not throw or ghost
auto is_normal = [](const cfg::Edge* e) {
return e->type() == cfg::EDGE_GOTO || e->type() == cfg::EDGE_BRANCH;
};
// Data structure that holds a possible target block, together with a set out
// registers that would have been assigned along the way to the target block.
struct TargetAndAssignedRegs {
cfg::Block* target;
std::unordered_set<reg_t> assigned_regs;
};
// Helper function that computs (ordered) list of unconditional target blocks,
// together with the sets of assigned registers.
auto get_unconditional_targets =
[&intra_cp, &cfg, &is_normal,
&env](cfg::Edge* succ_edge) -> std::vector<TargetAndAssignedRegs> {
auto succ_env = intra_cp.analyze_edge(succ_edge, env);
if (succ_env.is_bottom()) {
return {};
}
std::vector<TargetAndAssignedRegs> unconditional_targets{
(TargetAndAssignedRegs){succ_edge->target(), {}}};
std::unordered_set<cfg::Block*> visited;
while (true) {
auto& last_unconditional_target = unconditional_targets.back();
auto succ = last_unconditional_target.target;
if (!visited.insert(succ).second) {
// We found a loop; give up.
return {};
}
// We'll have to add to the set of assigned regs, so we make an
// intentional copy here
auto assigned_regs = last_unconditional_target.assigned_regs;
auto last_insn = succ->get_last_insn();
for (auto& mie : InstructionIterable(succ)) {
auto insn = mie.insn;
if (opcode::is_branch(insn->opcode())) {
continue;
}
// TODO: Support side-effect-free instruction sequences involving
// move-result(-pseudo), similar to what LocalDCE does
if (opcode::has_side_effects(insn->opcode()) || !insn->has_dest() ||
opcode::is_move_result_any(insn->opcode())) {
TRACE(CONSTP, 5, "forward_targets cannot follow %s",
SHOW(insn->opcode()));
// We stop the analysis here.
return unconditional_targets;
}
assigned_regs.insert(insn->dest());
intra_cp.analyze_instruction(insn, &succ_env, insn == last_insn->insn);
always_assert(!succ_env.is_bottom());
}
boost::optional<std::pair<cfg::Block*, ConstantEnvironment>>
only_feasible;
for (auto succ_succ_edge : cfg.get_succ_edges_if(succ, is_normal)) {
auto succ_succ_env = intra_cp.analyze_edge(succ_succ_edge, succ_env);
if (succ_succ_env.is_bottom()) {
continue;
}
if (only_feasible) {
// Found another one that's feasible, so there's not just a single
// feasible successor. We stop the analysis here.
return unconditional_targets;
}
only_feasible = std::make_pair(succ_succ_edge->target(), succ_succ_env);
}
unconditional_targets.push_back(
(TargetAndAssignedRegs){only_feasible->first, assigned_regs});
succ_env = only_feasible->second;
}
};
// Helper to check if any assigned register is live at the target block
auto is_any_assigned_reg_live_at_target =
[&liveness_fixpoint_iter,
&cfg](const TargetAndAssignedRegs& unconditional_target) {
auto& assigned_regs = unconditional_target.assigned_regs;
if (assigned_regs.empty()) {
return false;
}
if (!liveness_fixpoint_iter) {
liveness_fixpoint_iter.reset(new LivenessFixpointIterator(cfg));
liveness_fixpoint_iter->run(LivenessDomain());
}
auto live_in_vars = liveness_fixpoint_iter->get_live_in_vars_at(
unconditional_target.target);
if (live_in_vars.is_bottom()) {
// Could happen after having applied other transformations already
return true;
}
always_assert(!live_in_vars.is_top());
auto& elements = live_in_vars.elements();
return std::find_if(assigned_regs.begin(), assigned_regs.end(),
[&elements](reg_t reg) {
return elements.contains(reg);
}) != assigned_regs.end();
};
// Helper function to find furthest feasible target block for which no
// assigned regs are live-in
auto get_furthest_target_without_live_assigned_regs =
[&is_any_assigned_reg_live_at_target](
const std::vector<TargetAndAssignedRegs>& unconditional_targets)
-> cfg::Block* {
// The first (if any) unconditional target isn't interesting, as that's the
// one that's already currently on the cfg edge
if (unconditional_targets.size() <= 1) {
return nullptr;
}
// Find last successor where no assigned reg is live
for (int i = unconditional_targets.size() - 1; i >= 1; --i) {
auto& unconditional_target = unconditional_targets.at(i);
if (is_any_assigned_reg_live_at_target(unconditional_target)) {
continue;
}
TRACE(CONSTP, 2,
"forward_targets rewrites target, skipping %d targets, discharged "
"%zu assigned regs",
i, unconditional_target.assigned_regs.size());
return unconditional_target.target;
}
return nullptr;
};
// Main loop over, analyzing and potentially rewriting all normal successor
// edges to the furthest unconditional feasible target
for (auto succ_edge : cfg.get_succ_edges_if(block, is_normal)) {
auto unconditional_targets = get_unconditional_targets(succ_edge);
auto new_target =
get_furthest_target_without_live_assigned_regs(unconditional_targets);
if (!new_target) {
continue;
}
// Found (last) successor where no assigned reg is live -- forward to
// there
cfg.set_edge_target(succ_edge, new_target);
++m_stats.branches_forwarded;
}
// TODO: Forwarding may leave behind trivial conditional branches that can
// be folded.
}
bool Transform::has_problematic_return(cfg::ControlFlowGraph& cfg,
bool is_static,
DexType* declaring_type,
DexProto* proto,
const XStoreRefs* xstores) {
// Nothing to check without method information
if (!declaring_type || !proto) {
return false;
}
// No return issues when rtype is primitive
auto rtype = proto->get_rtype();
if (type::is_primitive(rtype)) {
return false;
}
// No return issues when there are no try/catch blocks
auto blocks = cfg.blocks();
bool has_catch =
std::find_if(blocks.begin(), blocks.end(), [](cfg::Block* block) {
return block->is_catch();
}) != blocks.end();
if (!has_catch) {
return false;
}
// For all return instructions, check whether the reaching definitions are of
// a type that's unavailable/external, or defined in a different store.
auto declaring_class_idx = xstores->get_store_idx(declaring_type);
auto is_problematic_return_type = [&](const DexType* t, IRInstruction* insn) {
t = type::get_element_type_if_array(t);
if (!type_class_internal(t)) {
// An unavailable or external class
TRACE(CONSTP, 2,
"Skipping {%s::%s} because {%s} is unavailable/external in {%s}",
SHOW(declaring_type), SHOW(proto), SHOW(t), SHOW(insn));
return true;
}
if (!xstores) {
return false;
}
auto t_idx = xstores->get_store_idx(t);
if (t_idx == declaring_class_idx) {
return false;
}
TRACE(CONSTP, 2,
"Skipping {%s::%s} because {%s} is from different store (%zu vs %zu) "
"in "
"{%s}",
SHOW(declaring_type), SHOW(proto), SHOW(t), declaring_class_idx,
t_idx, SHOW(insn));
return true;
};
reaching_defs::MoveAwareFixpointIterator fp_iter(cfg);
fp_iter.run({});
std::unique_ptr<type_inference::TypeInference> ti;
for (cfg::Block* block : blocks) {
auto env = fp_iter.get_entry_state_at(block);
if (env.is_bottom()) {
continue;
}
for (auto& mie : InstructionIterable(block)) {
IRInstruction* insn = mie.insn;
if (opcode::is_a_return(insn->opcode())) {
auto defs = env.get(insn->src(0));
always_assert(!defs.is_bottom() && !defs.is_top());
for (auto def : defs.elements()) {
auto op = def->opcode();
if (def->has_type()) {
if (is_problematic_return_type(def->get_type(), def)) {
return true;
}
} else if (def->has_method()) {
always_assert(opcode::is_an_invoke(op));
if (is_problematic_return_type(
def->get_method()->get_proto()->get_rtype(), def)) {
return true;
}
} else if (op == OPCODE_IGET_OBJECT || op == OPCODE_SGET_OBJECT) {
if (is_problematic_return_type(def->get_field()->get_type(), def)) {
return true;
}
} else if (op == OPCODE_AGET_OBJECT) {
if (!ti) {
ti.reset(new type_inference::TypeInference(cfg));
ti->run(is_static, declaring_type, proto->get_args());
}
auto& type_environments = ti->get_type_environments();
auto& type_environment = type_environments.at(def);
auto dex_type = type_environment.get_dex_type(def->src(1));
if (dex_type && type::is_array(*dex_type) &&
is_problematic_return_type(
type::get_array_component_type(*dex_type), def)) {
return true;
}
}
}
}
fp_iter.analyze_instruction(insn, &env);
}
}
return false;
}
void Transform::legacy_apply_forward_targets(
const intraprocedural::FixpointIterator& intra_cp,
cfg::ControlFlowGraph& cfg,
bool is_static,
DexType* declaring_type,
DexProto* proto,
const XStoreRefs* xstores) {
cfg.calculate_exit_block();
// The following is an attempt to avoid creating a control-flow structure that
// triggers the Android bug described in T55782799, related to a return
// statement in a try region when a type is unavailable/external, possibly
// from a different store.
// Besides that Android bug, it really shouldn't be necessary to do anything
// special about unavailable types or cross-store references here.
if (has_problematic_return(cfg, is_static, declaring_type, proto, xstores)) {
return;
}
// Note that the given intra_cp might not be aware of all blocks that exist in
// the cfg.
std::unique_ptr<LivenessFixpointIterator> liveness_fixpoint_iter;
for (auto block : cfg.blocks()) {
auto env = intra_cp.get_exit_state_at(block);
if (env.is_bottom()) {
// We found an unreachable block, or one that was added the cfg after
// intra_cp has run; just ignore it.
continue;
}
forward_targets(intra_cp, env, cfg, block, liveness_fixpoint_iter);
}
}
void Transform::Stats::log_metrics(ScopedMetrics& sm, bool with_scope) const {
using OptScope = boost::optional<ScopedMetrics::Scope>;
OptScope scope = with_scope ? OptScope(sm.scope("const_prop")) : boost::none;
sm.set_metric("branches_forwarded", branches_forwarded);
sm.set_metric("branch_propagated", branches_removed);
sm.set_metric("materialized_consts", materialized_consts);
sm.set_metric("throws", throws);
sm.set_metric("null_checks", null_checks);
sm.set_metric("null_checks_method_calls", null_checks_method_calls);
sm.set_metric("unreachable_instructions_removed",
unreachable_instructions_removed);
sm.set_metric("redundant_puts_removed", redundant_puts_removed);
TRACE(CONSTP, 3, "Null checks removed: %zu(%zu)", null_checks,
null_checks_method_calls);
sm.set_metric("added_param_const", added_param_const);
}
} // namespace constant_propagation