opt/up-code-motion/UpCodeMotion.cpp (286 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 eliminates gotos by moving trivial instructions such a consts and * moves before a conditional branch. * * For example: * * IF_EQZ v2, L1 * CONST v0, 1 * ... (GOTO elsewhere or RETURN or THROW) * L1: CONST v0, 0 // where L1 is only reachable via the above IF-instruction * GOTO L2 * * becomes * * CONST v0, 0 * IF_EQZ v2, L2 * CONST v0, 1 * ... * */ #include "UpCodeMotion.h" #include <vector> #include "ControlFlow.h" #include "DexClass.h" #include "DexUtil.h" #include "IRCode.h" #include "IRInstruction.h" #include "IROpcode.h" #include "PassManager.h" #include "RedexContext.h" #include "Show.h" #include "SourceBlocks.h" #include "Trace.h" #include "TypeInference.h" #include "Walkers.h" namespace { constexpr const char* METRIC_INSTRUCTIONS_MOVED = "num_instructions_moved"; constexpr const char* METRIC_BRANCHES_MOVED_OVER = "num_branches_moved_over"; constexpr const char* METRIC_INVERTED_CONDITIONAL_BRANCHES = "num_inverted_conditional_branches"; constexpr const char* METRIC_CLOBBERED_REGISTERS = "num_clobbered_registers"; constexpr const char* METRIC_SKIPPED_BRANCHES = "num_skipped_branches"; } // namespace // Helper function that checks if a branch is hot. // Here we assume that : // 1. if a represenative block is hit, the rest of source blocks are also // covered. // 2. if a represenative block is hit via any one interaction, it is considered // to be "hot" Potentially introduce hotness threshhold here. bool UpCodeMotionPass::is_hot(cfg::Block* b) { const auto* rep_block = source_blocks::get_first_source_block(b); if (rep_block == nullptr) { return false; } bool is_hot = false; rep_block->foreach_val_early([&is_hot](const auto& val) { is_hot = (val && val->val > 0.0f); return is_hot; }); return is_hot; } // Helper function that scans a block for leading const and move instructions, // and returning a value that indicates whether there's no other kind of // instruction in the block. bool UpCodeMotionPass::gather_movable_instructions( cfg::Block* b, std::vector<IRInstruction*>* instructions) { for (auto& mie : InstructionIterable(b)) { auto insn = mie.insn; // We really only support at this time... // - const, not const-wide, const-class, or const-string. // - move and move-object, not move-wide // - other trivial side-effect free computations that are not wide switch (insn->opcode()) { case OPCODE_NOP: continue; case OPCODE_CONST: case OPCODE_MOVE: case OPCODE_MOVE_OBJECT: case OPCODE_NEG_INT: case OPCODE_NOT_INT: case OPCODE_NEG_FLOAT: case OPCODE_INT_TO_FLOAT: case OPCODE_FLOAT_TO_INT: case OPCODE_INT_TO_BYTE: case OPCODE_INT_TO_CHAR: case OPCODE_INT_TO_SHORT: case OPCODE_CMPL_FLOAT: case OPCODE_CMPG_FLOAT: 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_SHL_INT: case OPCODE_SHR_INT: case OPCODE_USHR_INT: case OPCODE_ADD_INT_LIT16: case OPCODE_RSUB_INT: case OPCODE_MUL_INT_LIT16: case OPCODE_AND_INT_LIT16: case OPCODE_OR_INT_LIT16: case OPCODE_XOR_INT_LIT16: case OPCODE_ADD_INT_LIT8: case OPCODE_RSUB_INT_LIT8: case OPCODE_MUL_INT_LIT8: case OPCODE_AND_INT_LIT8: case OPCODE_OR_INT_LIT8: case OPCODE_XOR_INT_LIT8: case OPCODE_SHL_INT_LIT8: case OPCODE_SHR_INT_LIT8: case OPCODE_USHR_INT_LIT8: instructions->push_back(insn); continue; default: return false; } } return true; } // Helper function that, given a branch and a goto edge, figures out if all // movable instructions of the branch edge target block have a matching // (same dest register) leading instruction in the goto edge target block, // and that move-instructions don't read what's written. bool UpCodeMotionPass::gather_instructions_to_insert( cfg::Edge* branch_edge, cfg::Edge* goto_edge, std::vector<IRInstruction*>* instructions_to_insert) { cfg::Block* branch_block = branch_edge->target(); // The branch edge target block must end in a goto, and // have a unique predecessor. if (branch_block->branchingness() != opcode::BRANCH_GOTO || branch_block->preds().size() != 1) { TRACE(UCM, 5, "[up code motion] giving up: branch block"); return false; } std::vector<IRInstruction*> ordered_branch_instructions; // Gather all of the movable instructions of the branch edge // target block; give up when there are any other instructions. if (!gather_movable_instructions(branch_block, &ordered_branch_instructions)) { TRACE(UCM, 5, "[up code motion] giving up: gather"); return false; } cfg::Block* goto_block = goto_edge->target(); std::vector<IRInstruction*> ordered_instructions_in_goto_block; // Gather all of the movable instructions of the branch edge // goto block; it's okay if there are other trailing instructions. gather_movable_instructions(goto_block, &ordered_instructions_in_goto_block); // In the following, we check if all the registers assigned to by // movable instructions of the branch edge target block also // get assigned by the goto edge target block. std::unordered_map<uint32_t, size_t> goto_instruction_ends; for (size_t i = 0; i < ordered_instructions_in_goto_block.size(); i++) { IRInstruction* insn = ordered_instructions_in_goto_block[i]; // only the first emplace for a particular register will stick goto_instruction_ends.emplace(insn->dest(), i + 1); } std::unordered_set<uint32_t> destroyed_dests; size_t ordered_instructions_in_goto_block_index_end = 0; for (IRInstruction* insn : ordered_branch_instructions) { uint32_t dest = insn->dest(); destroyed_dests.insert(dest); auto it = goto_instruction_ends.find(dest); if (it == goto_instruction_ends.end()) { TRACE(UCM, 5, "[up code motion] giving up: branch instruction assigns to " "dest with no corresponding goto instructions"); return false; } ordered_instructions_in_goto_block_index_end = std::max(ordered_instructions_in_goto_block_index_end, it->second); } if (destroyed_dests.empty()) { return false; } // Do the goto-instructions need any src that the branch-instructions // destroy? for (size_t i = 0; i < ordered_instructions_in_goto_block_index_end; i++) { IRInstruction* insn = ordered_instructions_in_goto_block[i]; for (auto src : insn->srcs()) { if (destroyed_dests.count(src)) { TRACE(UCM, 5, "[up code motion] giving up: goto source overlaps with " "branch dest"); return false; } } destroyed_dests.erase(insn->dest()); } // All tests passed. Let's populate instructions_to_insert... for (IRInstruction* insn : ordered_branch_instructions) { insn = new IRInstruction(*insn); instructions_to_insert->push_back(insn); } return true; } UpCodeMotionPass::Stats UpCodeMotionPass::process_code( bool is_static, DexType* declaring_type, DexTypeList* args, IRCode* code, bool is_branch_hot_check) { Stats stats; code->build_cfg(/* editable = true*/); auto& cfg = code->cfg(); std::unique_ptr<type_inference::TypeInference> type_inference; std::unordered_set<cfg::Block*> blocks_to_remove_set; std::vector<cfg::Block*> blocks_to_remove; for (cfg::Block* b : cfg.blocks()) { if (blocks_to_remove_set.count(b)) { continue; } auto br = b->branchingness(); if (br != opcode::BRANCH_IF) { continue; } auto last_insn_it = b->get_last_insn(); always_assert(last_insn_it != b->end()); auto if_insn = last_insn_it->insn; always_assert(opcode::is_a_conditional_branch(if_insn->opcode())); always_assert(!if_insn->is_wide()); // We found a block that ends with a conditional branch. // Let's see if our transformation can be applied. cfg::Edge* branch_edge = cfg.get_succ_edge_of_type(b, cfg::EDGE_BRANCH); always_assert(branch_edge != nullptr); cfg::Edge* goto_edge = cfg.get_succ_edge_of_type(b, cfg::EDGE_GOTO); always_assert(goto_edge != nullptr); std::vector<IRInstruction*> instructions_to_insert; std::vector<uint32_t> clobbered_regs; // Can we do our code transformation directly? if (!gather_instructions_to_insert(branch_edge, goto_edge, &instructions_to_insert)) { // Or do we first have to flip the conditional branch? if (!gather_instructions_to_insert(goto_edge, branch_edge, &instructions_to_insert)) { // We just can't do it. continue; } // Flip conditional branch before doing actual transformation. if_insn->set_opcode(opcode::invert_conditional_branch(if_insn->opcode())); // swap goto and branch target cfg::Block* branch_target = branch_edge->target(); cfg::Block* goto_target = goto_edge->target(); cfg.set_edge_target(branch_edge, goto_target); cfg.set_edge_target(goto_edge, branch_target); stats.inverted_conditional_branches++; } if (is_branch_hot_check) { if (is_hot(b)) { if (!is_hot(branch_edge->target())) { stats.skipped_branches++; continue; } } } // We want to insert the (cloned) movable instructions of the branch edge // target block just in front of the if-instruction. However, if the if- // instruction reads from the same registers that the movable // instructions write to, then we have a problem. To work around that // problem, we move the problematic registers used by the if-instruction to // new temp registers, and then rewrite the if-instruction to use the new // temp register. Even though the new move instructions increase code size // here, this is largely undone later by register allocation + copy // propagation. std::unordered_map<uint32_t, uint32_t> temps; for (auto instruction_to_insert : instructions_to_insert) { auto dest = instruction_to_insert->dest(); const auto& srcs = if_insn->srcs(); for (size_t i = 0; i < srcs.size(); i++) { if (srcs[i] == dest) { uint32_t temp; auto temp_it = temps.find(dest); if (temp_it != temps.end()) { temp = temp_it->second; } else { if (!type_inference) { // We run the type inference once, and reuse results within this // method. This is okay, even though we mutate the cfg, because // we don't change the set of if-instructions, and only do per- // instructions lookups in the type environments. type_inference.reset(new type_inference::TypeInference(cfg)); type_inference->run(is_static, declaring_type, args); } auto& type_environments = type_inference->get_type_environments(); auto& type_environment = type_environments.at(if_insn); auto type = type_environment.get_type(dest); always_assert(!type.is_top() && !type.is_bottom()); temp = cfg.allocate_temp(); auto it = b->to_cfg_instruction_iterator(last_insn_it); IRInstruction* move_insn = new IRInstruction( type.element() == REFERENCE ? OPCODE_MOVE_OBJECT : OPCODE_MOVE); move_insn->set_src(0, dest)->set_dest(temp); cfg.insert_before(it, move_insn); stats.clobbered_registers++; temps.emplace(dest, temp); } if_insn->set_src(i, temp); } } } // Okay, we can apply our transformation: // We insert the (cloned) movable instructions of the branch edge target // block just in front of the if-instruction. // And then we remove the branch edge target block, rewiring the branch edge // to point to the goto target of the branch edge target block. cfg::Block* branch_block = branch_edge->target(); for (IRInstruction* insn : instructions_to_insert) { auto it = b->to_cfg_instruction_iterator(last_insn_it); cfg.insert_before(it, insn); } cfg.set_edge_target(branch_edge, branch_block->goes_to()); always_assert(!blocks_to_remove_set.count(branch_block)); blocks_to_remove_set.insert(branch_block); blocks_to_remove.push_back(branch_block); stats.instructions_moved += instructions_to_insert.size(); stats.branches_moved_over++; } cfg.remove_blocks(blocks_to_remove); code->clear_cfg(); return stats; } void UpCodeMotionPass::run_pass(DexStoresVector& stores, ConfigFiles& /* unused */, PassManager& mgr) { auto scope = build_class_scope(stores); Stats stats = walk::parallel::methods<Stats>(scope, [&](DexMethod* method) { const auto code = method->get_code(); if (!code) { return Stats{}; } Stats stats_lambda = UpCodeMotionPass::process_code( is_static(method), method->get_class(), method->get_proto()->get_args(), code, m_check_if_branch_is_hot); if (stats_lambda.instructions_moved || stats_lambda.branches_moved_over) { TRACE(UCM, 3, "[up code motion] Moved %zu instructions over %zu conditional " "branches while inverting %zu conditional branches and dealing " "with %zu cold branches and %zu clobbered registers in {%s}", stats_lambda.instructions_moved, stats_lambda.branches_moved_over, stats_lambda.inverted_conditional_branches, stats_lambda.skipped_branches, stats_lambda.clobbered_registers, SHOW(method)); } return stats_lambda; }); mgr.incr_metric(METRIC_INSTRUCTIONS_MOVED, stats.instructions_moved); mgr.incr_metric(METRIC_BRANCHES_MOVED_OVER, stats.branches_moved_over); mgr.incr_metric(METRIC_INVERTED_CONDITIONAL_BRANCHES, stats.inverted_conditional_branches); mgr.incr_metric(METRIC_SKIPPED_BRANCHES, stats.skipped_branches); mgr.incr_metric(METRIC_CLOBBERED_REGISTERS, stats.clobbered_registers); TRACE(UCM, 1, "[up code motion] Moved %zu instructions over %zu conditional branches " "while inverting %zu conditional branches and dealing with %zu cold " "branches and %zu clobbered registers in total", stats.instructions_moved, stats.branches_moved_over, stats.inverted_conditional_branches, stats.skipped_branches, stats.clobbered_registers); } static UpCodeMotionPass s_pass;