void OSRPass::_run(IRManager & irm) { splitCriticalEdges(irm); computeDominatorsAndLoops(irm); LoopTree* loopTree = irm.getLoopTree(); DominatorTree* dominatorTree = irm.getDominatorTree(); OSR osr(irm, irm.getMemoryManager(), loopTree, dominatorTree); osr.runPass(); }
/* * For all guard instructions in unit, check to see if we can relax the * destination type to something less specific. The GuardConstraints map * contains information about what properties of the guarded type matter for * each instruction. If simple is true, guards will not be relaxed past * DataTypeSpecific except guards which are relaxed all the way to * DataTypeGeneric. Returns true iff any changes were made to the trace. */ bool relaxGuards(IRUnit& unit, const GuardConstraints& guards, bool simple) { Timer _t("optimize_relaxGuards"); splitCriticalEdges(unit); auto blocks = rpoSortCfg(unit); auto changed = false; for (auto* block : blocks) { for (auto& inst : *block) { if (!isGuardOp(inst.op())) continue; auto it = guards.find(&inst); auto constraint = it == guards.end() ? TypeConstraint() : it->second; FTRACE(2, "relaxGuards processing {} with constraint {}\n", inst, constraint); if (simple && constraint.category > DataTypeGeneric && constraint.category < DataTypeSpecific) { constraint.category = DataTypeSpecific; } auto const oldType = inst.typeParam(); auto newType = relaxType(oldType, constraint); // Sometimes we (legitimately) end up with a guard like this: // // t4:StkPtr = GuardStk<BoxedArr,0,<DataTypeGeneric, // inner:DataTypeSpecific, // Type::BoxedCell>> t2:StkPtr // // The outer category is DataTypeGeneric because we know from eval stack // flavors that the top of the stack here is always boxed. The inner // category is DataTypeSpecific, indicating we care what the inner type // is, even though it's just a hint. If we treated this like any other // guard, we would relax the typeParam to Type::Gen and insert an assert // to Type::BoxedCell right after it. Unfortunately, this loses the hint // that the inner type is Arr. Eventually we should have some side // channel for passing around hints for inner ref types, but for now the // best we can do is forcibly keep the guard around, preserving the inner // type hint. if (constraint.assertedType.isBoxed() && oldType < constraint.assertedType) { auto relaxedInner = relaxInner(oldType, constraint); if (relaxedInner < Type::BoxedCell && newType >= Type::BoxedCell) { FTRACE(1, "relaxGuards changing newType to {}\n", newType); newType = relaxedInner; } } if (constraint.assertedType < newType) { // If the asserted type is more specific than the new guarded type, set // the guard to the relaxed type but insert an assert operation between // the instruction and its dst. We go from something like this: // // t5:FramePtr = GuardLoc<Int, 4, <DataTypeGeneric,Int>> t4:FramePtr // // to this: // // t6:FramePtr = GuardLoc<Gen, 4> t4:FramePtr // t5:FramePtr = AssertLoc<Int, 4> t6:FramePtr auto* oldDst = inst.dst(); auto* newDst = unit.genDst(&inst); auto* newAssert = [&] { switch (inst.op()) { case GuardLoc: case CheckLoc: return unit.genWithDst(oldDst, guardToAssert(inst.op()), inst.marker(), *inst.extra<LocalId>(), constraint.assertedType, newDst); case GuardStk: case CheckStk: return unit.genWithDst(oldDst, guardToAssert(inst.op()), inst.marker(), *inst.extra<StackOffset>(), constraint.assertedType, newDst); case CheckType: return unit.genWithDst(oldDst, guardToAssert(inst.op()), inst.marker(), constraint.assertedType, newDst); default: always_assert(false); } }(); FTRACE(1, "relaxGuards inserting {} between {} and its dst, " "changing typeParam to {}\n", *newAssert, inst, newType); inst.setTypeParam(newType); // Now, insert the assert after the guard. For control flow guards, // this means inserting it on the next edge. if (inst.isControlFlow()) { auto* block = inst.next(); block->insert(block->skipHeader(), newAssert); } else { auto* block = inst.block(); auto it = block->iteratorTo(&inst); ++it; block->insert(it, newAssert); } changed = true; } else if (oldType != newType) { FTRACE(1, "relaxGuards changing {}'s type to {}\n", inst, newType); inst.setTypeParam(newType); changed = true; } } } if (!changed) return false; // Make a second pass to reflow types, with some special logic for loads. FrameState state(unit); for (auto* block : blocks) { state.startBlock(block); for (auto& inst : *block) { state.setMarker(inst.marker()); copyProp(&inst); visitLoad(&inst, state); if (!removeGuard(unit, &inst, state)) { retypeDests(&inst); state.update(&inst); } } state.finishBlock(block); } return true; }
/* * For all guard instructions in unit, check to see if we can relax the * destination type to something less specific. The GuardConstraints map * contains information about what properties of the guarded type matter for * each instruction. If simple is true, guards will not be relaxed past * DataTypeSpecific except guards which are relaxed all the way to * DataTypeGeneric. Returns true iff any changes were made to the trace. */ bool relaxGuards(IRUnit& unit, const GuardConstraints& constraints, RelaxGuardsFlags flags) { Timer _t(Timer::optimize_relaxGuards); ITRACE(2, "entering relaxGuards\n"); Indent _i; bool simple = flags & RelaxSimple; bool reflow = flags & RelaxReflow; splitCriticalEdges(unit); auto& guards = constraints.guards; auto blocks = rpoSortCfg(unit); auto changed = false; for (auto* block : blocks) { for (auto& inst : *block) { if (!isGuardOp(inst.op())) continue; auto it = guards.find(&inst); auto constraint = it == guards.end() ? TypeConstraint() : it->second; ITRACE(2, "relaxGuards processing {} with constraint {}\n", inst, constraint); auto simplifyCategory = [simple](DataTypeCategory& cat) { if (simple && cat > DataTypeGeneric && cat < DataTypeSpecific) { cat = DataTypeSpecific; } }; simplifyCategory(constraint.category); simplifyCategory(constraint.innerCat); auto const oldType = inst.typeParam(); auto newType = relaxType(oldType, constraint); if (oldType != newType) { ITRACE(1, "relaxGuards changing {}'s type to {}\n", inst, newType); inst.setTypeParam(newType); changed = true; } } } if (!changed) return false; if (!reflow) return true; // Make a second pass to reflow types, with some special logic for loads. FrameState state{unit, unit.entry()->front().marker()}; for (auto* block : blocks) { ITRACE(2, "relaxGuards reflow entering B{}\n", block->id()); Indent _i; state.startBlock(block, block->front().marker()); for (auto& inst : *block) { state.setMarker(inst.marker()); copyProp(&inst); visitLoad(&inst, state); retypeDests(&inst, &unit); state.update(&inst); } state.finishBlock(block); } return true; }
void optimizeStores(IRUnit& unit) { PassTracer tracer{&unit, Trace::hhir_store, "optimizeStores"}; // This isn't required for correctness, but it may allow removing stores that // otherwise we would leave alone. splitCriticalEdges(unit); auto incompleteQ = dataflow_worklist<PostOrderId>(unit.numBlocks()); /* * Global state for this pass, visible while processing any block. */ auto genv = Global { unit }; auto const& poBlockList = genv.poBlockList; if (genv.ainfo.locations.size() == 0) { FTRACE(1, "no memory accesses to possibly optimize\n"); return; } FTRACE(1, "\nLocations:\n{}\n", show(genv.ainfo)); /* * Initialize the block state structures. * * Important note: every block starts with an empty liveOut set, including * blocks that are exiting the region. When an HHIR region is exited, * there's always some instruction we can use to indicate via memory_effects * what may be read (e.g. EndCatch, RetCtrl, ReqBindJmp, etc). When we start * iterating, we'll appropriately add things to GEN based on these. */ for (auto poId = uint32_t{0}; poId < poBlockList.size(); ++poId) { genv.blockStates[poBlockList[poId]->id()].id = poId; incompleteQ.push(poId); } /* * Analyze each block to compute its transfer function. * * The blockAnalysis vector is indexed by post order id. */ auto const blockAnalysis = [&] () -> jit::vector<BlockAnalysis> { auto ret = jit::vector<BlockAnalysis>{}; ret.reserve(unit.numBlocks()); for (auto id = uint32_t{0}; id < poBlockList.size(); ++id) { ret.push_back(analyze_block(genv, poBlockList[id])); } return ret; }(); FTRACE(2, "Transfer functions:\n{}\n", [&]() -> std::string { auto ret = std::string{}; for (auto poId = uint32_t{0}; poId < poBlockList.size(); ++poId) { auto& analysis = blockAnalysis[poId]; folly::format( &ret, " B{}\n" " gen: {}\n" " kill: {}\n", poBlockList[poId]->id(), show(analysis.gen), show(analysis.kill) ); } return ret; }() ); /* * Iterate on the liveOut states until we reach a fixed point. */ FTRACE(4, "Iterating\n"); while (!incompleteQ.empty()) { auto const poId = incompleteQ.pop(); auto const blk = poBlockList[poId]; auto& state = genv.blockStates[blk->id()]; auto const transfer = blockAnalysis[poId]; assertx(state.id == poId); state.liveIn = transfer.gen | (state.liveOut & ~transfer.kill); FTRACE(4, " block B{}\n" " live out: {}\n" " gen : {}\n" " kill : {}\n" " live in : {}\n", blk->id(), show(state.liveOut), show(transfer.gen), show(transfer.kill), show(state.liveIn)); /* * Update predecessors by merging the live in state into the live out state * of each predecessor. * * If anything changes, reschedule the predecessor. */ blk->forEachPred([&] (Block* pred) { FTRACE(4, " -> {}\n", pred->id()); auto& predState = genv.blockStates[pred->id()]; auto const oldLiveOut = predState.liveOut; predState.liveOut |= state.liveIn; if (predState.liveOut != oldLiveOut) { incompleteQ.push(predState.id); } }); } /* * We've reached a fixed point. Now we can act on this information to remove * dead stores. */ FTRACE(2, "\nFixed point:\n{}\n", [&]() -> std::string { auto ret = std::string{}; for (auto& blk : poBlockList) { folly::format( &ret, " B{: <3}: {}\n", blk->id(), show(genv.blockStates[blk->id()].liveOut) ); } return ret; }() ); for (auto& block : poBlockList) { optimize_block(genv, block); } }