opt/object-sensitive-dce/UsedVarsAnalysis.cpp (214 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 "UsedVarsAnalysis.h"
#include <utility>
#include "ReachableClasses.h"
#include "Show.h"
#include "Trace.h"
namespace ptrs = local_pointers;
namespace {
/*
* Record the environment before the execution of every instruction. We need
* this data during the backwards used vars analysis.
*/
std::unordered_map<const IRInstruction*, ptrs::Environment>
gen_instruction_environment_map(const cfg::ControlFlowGraph& cfg,
const ptrs::FixpointIterator& fp_iter) {
std::unordered_map<const IRInstruction*, ptrs::Environment> result;
for (auto* block : cfg.blocks()) {
auto env = fp_iter.get_entry_state_at(block);
for (auto& mie : InstructionIterable(block)) {
auto* insn = mie.insn;
result.emplace(insn, env);
fp_iter.analyze_instruction(insn, &env);
}
}
return result;
}
} // namespace
namespace used_vars {
using namespace ir_analyzer;
FixpointIterator::FixpointIterator(
const local_pointers::FixpointIterator& pointers_fp_iter,
side_effects::InvokeToSummaryMap invoke_to_summary_map,
const cfg::ControlFlowGraph& cfg)
: BaseBackwardsIRAnalyzer<UsedVarsSet>(cfg),
m_insn_env_map(gen_instruction_environment_map(cfg, pointers_fp_iter)),
m_invoke_to_summary_map(std::move(invoke_to_summary_map)) {}
void FixpointIterator::analyze_instruction(IRInstruction* insn,
UsedVarsSet* used_vars) const {
TRACE(OSDCE, 5, "Before %s : %s", SHOW(insn), SHOW(*used_vars));
bool required = is_required(insn, *used_vars);
auto op = insn->opcode();
if (ptrs::may_alloc(op)) {
used_vars->remove(insn);
}
if (insn->has_dest()) {
used_vars->remove(insn->dest());
} else if (insn->has_move_result_any()) {
used_vars->remove(RESULT_REGISTER);
}
if (required) {
const auto& env = m_insn_env_map.at(insn);
if (env.is_bottom()) {
return;
}
// We mark all src registers -- and any pointers they contain -- as used,
// even if we don't read from the pointee objects. This is done in order to
// correctly handle the verifier's requirement that all objects are
// initialized before being used (even if only to make unused writes to them
// or to check whether the pointer is non-null.) Marking modified objects as
// used ensures that we don't delete the <init>() calls on them. See the
// UsedVarsTest_noDeleteInit unit test for a concrete example.
for (size_t i = 0; i < insn->srcs_size(); ++i) {
auto reg = insn->src(i);
used_vars->add(reg);
auto pointers = env.get_pointers(reg);
if (!pointers.is_value()) {
continue;
}
for (auto pointer : pointers.elements()) {
if (ptrs::may_alloc(pointer->opcode())) {
used_vars->add(pointer);
}
}
}
if (opcode::is_move_result_any(op)) {
used_vars->add(RESULT_REGISTER);
}
}
TRACE(OSDCE, 5, "After: %s", SHOW(*used_vars));
}
bool FixpointIterator::is_used_or_escaping_write(const ptrs::Environment& env,
const UsedVarsSet& used_vars,
reg_t obj_reg) const {
auto pointers = env.get_pointers(obj_reg);
if (!pointers.is_value()) {
return true;
}
for (auto pointer : pointers.elements()) {
if (pointer->opcode() == IOPCODE_LOAD_PARAM_OBJECT ||
used_vars.contains(pointer) || env.may_have_escaped(pointer)) {
return true;
}
}
return false;
}
bool FixpointIterator::is_required(const IRInstruction* insn,
const UsedVarsSet& used_vars) const {
auto op = insn->opcode();
switch (op) {
case IOPCODE_LOAD_PARAM:
case IOPCODE_LOAD_PARAM_OBJECT:
case IOPCODE_LOAD_PARAM_WIDE:
// Control-flow opcodes are always required.
case OPCODE_RETURN_VOID:
case OPCODE_RETURN:
case OPCODE_RETURN_WIDE:
case OPCODE_RETURN_OBJECT:
case OPCODE_MONITOR_ENTER:
case OPCODE_MONITOR_EXIT:
case OPCODE_CHECK_CAST:
case OPCODE_THROW:
case OPCODE_GOTO:
case OPCODE_SWITCH:
case OPCODE_IF_EQ:
case OPCODE_IF_NE:
case OPCODE_IF_LT:
case OPCODE_IF_GE:
case OPCODE_IF_GT:
case OPCODE_IF_LE:
case OPCODE_IF_EQZ:
case OPCODE_IF_NEZ:
case OPCODE_IF_LTZ:
case OPCODE_IF_GEZ:
case OPCODE_IF_GTZ:
case OPCODE_IF_LEZ: {
return true;
}
case OPCODE_APUT:
case OPCODE_APUT_WIDE:
case OPCODE_APUT_OBJECT:
case OPCODE_APUT_BOOLEAN:
case OPCODE_APUT_BYTE:
case OPCODE_APUT_CHAR:
case OPCODE_APUT_SHORT:
case OPCODE_IPUT:
case OPCODE_IPUT_WIDE:
case OPCODE_IPUT_OBJECT:
case OPCODE_IPUT_BOOLEAN:
case OPCODE_IPUT_BYTE:
case OPCODE_IPUT_CHAR:
case OPCODE_IPUT_SHORT: {
const auto& env = m_insn_env_map.at(insn);
return is_used_or_escaping_write(env, used_vars, insn->src(1));
}
case OPCODE_FILL_ARRAY_DATA: {
const auto& env = m_insn_env_map.at(insn);
return is_used_or_escaping_write(env, used_vars, insn->src(0));
}
case OPCODE_SPUT:
case OPCODE_SPUT_WIDE:
case OPCODE_SPUT_OBJECT:
case OPCODE_SPUT_BOOLEAN:
case OPCODE_SPUT_BYTE:
case OPCODE_SPUT_CHAR:
case OPCODE_SPUT_SHORT: {
return true;
}
case OPCODE_INVOKE_DIRECT:
case OPCODE_INVOKE_STATIC:
case OPCODE_INVOKE_VIRTUAL: {
auto method = resolve_method(insn->get_method(), opcode_to_search(insn));
if (method == nullptr) {
return true;
}
if (assumenosideeffects(method)) {
return used_vars.contains(RESULT_REGISTER);
}
const auto& env = m_insn_env_map.at(insn);
if (method::is_init(method) &&
(used_vars.contains(insn->src(0)) ||
is_used_or_escaping_write(env, used_vars, insn->src(0)))) {
return true;
}
if (!m_invoke_to_summary_map.count(insn)) {
return true;
}
// A call is required if it has a side-effect, if its return value is used,
// or if it mutates an argument that may later be read somewhere up the
// callstack.
auto& summary = m_invoke_to_summary_map.at(insn);
if (summary.effects != side_effects::EFF_NONE ||
used_vars.contains(RESULT_REGISTER)) {
return true;
}
const auto& mod_params = summary.modified_params;
return std::any_of(
mod_params.begin(), mod_params.end(), [&](param_idx_t idx) {
return is_used_or_escaping_write(env, used_vars, insn->src(idx));
});
}
case OPCODE_INVOKE_SUPER:
case OPCODE_INVOKE_INTERFACE: {
return true;
}
default: {
if (insn->has_dest()) {
return used_vars.contains(insn->dest());
} else if (insn->has_move_result_any()) {
return used_vars.contains(RESULT_REGISTER);
}
return true;
}
}
}
std::vector<IRList::iterator> get_dead_instructions(
const IRCode& const_code, const FixpointIterator& fp_iter) {
// We aren't mutating the IRCode object, but we want to return non-const
// IRList::iterators.
auto& code = const_cast<IRCode&>(const_code);
auto& cfg = code.cfg();
std::vector<IRList::iterator> dead_instructions;
for (auto* block : cfg.blocks()) {
auto used_vars = fp_iter.get_used_vars_at_exit(block);
TRACE(OSDCE, 5, "B%zu exit : %s", block->id(), SHOW(used_vars));
for (auto it = block->rbegin(); it != block->rend(); ++it) {
if (it->type != MFLOW_OPCODE) {
continue;
}
auto* insn = it->insn;
if (!fp_iter.is_required(insn, used_vars)) {
// move-result-pseudo instructions will be automatically removed
// when their primary instruction is deleted.
if (!opcode::is_a_move_result_pseudo(insn->opcode())) {
dead_instructions.emplace_back(code.iterator_to(*it));
}
}
fp_iter.analyze_instruction(insn, &used_vars);
}
TRACE(OSDCE, 5, "B%zu entry : %s", block->id(),
SHOW(fp_iter.get_used_vars_at_entry(block)));
}
return dead_instructions;
}
} // namespace used_vars