/hphp/hhbbc/dce.cpp
C++ | 1026 lines | 573 code | 115 blank | 338 comment | 71 complexity | 899dc962ec2bf32cb08ed74843e6d1c1 MD5 | raw file
- /*
- +----------------------------------------------------------------------+
- | HipHop for PHP |
- +----------------------------------------------------------------------+
- | Copyright (c) 2010-2014 Facebook, Inc. (http://www.facebook.com) |
- +----------------------------------------------------------------------+
- | This source file is subject to version 3.01 of the PHP license, |
- | that is bundled with this package in the file LICENSE, and is |
- | available through the world-wide-web at the following url: |
- | http://www.php.net/license/3_01.txt |
- | If you did not receive a copy of the PHP license and are unable to |
- | obtain it through the world-wide-web, please send a note to |
- | license@php.net so we can mail you a copy immediately. |
- +----------------------------------------------------------------------+
- */
- #include "hphp/hhbbc/dce.h"
- #include <vector>
- #include <string>
- #include <utility>
- #include <bitset>
- #include <sstream>
- #include <algorithm>
- #include <set>
- #include <boost/dynamic_bitset.hpp>
- #include <folly/gen/Base.h>
- #include <folly/gen/String.h>
- #include "hphp/util/trace.h"
- #include "hphp/util/dataflow-worklist.h"
- #include "hphp/hhbbc/representation.h"
- #include "hphp/hhbbc/analyze.h"
- #include "hphp/hhbbc/interp-state.h"
- #include "hphp/hhbbc/interp.h"
- #include "hphp/hhbbc/type-system.h"
- #include "hphp/hhbbc/unit-util.h"
- #include "hphp/hhbbc/cfg.h"
- namespace HPHP { namespace HHBBC {
- TRACE_SET_MOD(hhbbc_dce);
- //////////////////////////////////////////////////////////////////////
- /*
- * This module contains code to perform both local DCE and global DCE.
- *
- * The local DCE algorithm addresses dead eval stack manipulations and
- * dead stores to locals that are visible within a single basic block.
- *
- * The global DCE performs a liveness analysis and then uses this
- * information to allow dead stores to locals to be eliminated across
- * blocks. It does not attempt to remove unnecessary evaluation stack
- * manipulation spanning basic blocks, but it uses the same local DCE
- * code and will eliminate intra-block stack manipulations.
- *
- * Both types of DCE here need to be type-aware, but they must visit
- * blocks backward. They accomplish this by forward-propagating the
- * block input states from a FuncAnalysis to each instruction in the
- * block prior to doing the backward iteration.
- *
- * Eval stack:
- *
- * During backward traversal of a block, we maintain a "backwards"
- * stack, indicating which eval stack slots are going to be required
- * in the future or not.
- *
- * During this traversal, each instruction that pops when going
- * forward instead "pushes" information about whether that input
- * will be required. If it is not required, it also pushes an
- * accumulating set of instruction ids that must be removed if the
- * instruction which produces the stack slot is removed. (All
- * instructions in these sets must be removed if any are, in order
- * to keep the stack depths correct.)
- *
- * Similarly, each instruction that would push a value going forward
- * instead "pops" the information about whether its stack output is
- * going to be needed. If not, the instruction can mark itself (and
- * all downstream instructions that depended on it) as removable.
- *
- * Locals:
- *
- * While a block is iterated backward, the set of live locals is
- * tracked. The initial state of this live set depends on whether
- * we are performing global or local DCE, and in the local case
- * includes all locals in the function.
- *
- * When a local may be read, it is added to the live set. When a
- * local is definitely-written, it is removed from the set.
- *
- * If a instruction may write to a local that is not live, it can be
- * marked as removable if it is known to have no other side-effects.
- * Currently this is only hooked up to SetL.
- *
- * Liveness analysis:
- *
- * The global algorithm first performs a liveness analysis to
- * propagate live out sets to each block.
- *
- * This analysis is basically normal, but slightly modified from the
- * usual in order to deal with the way exceptional control flow is
- * represented in our CFG (with factored edges).
- *
- * It essentially is the liveness analysis algorithm described at
- * http://dl.acm.org/citation.cfm?id=316171, except that we don't
- * need to track kill sets for each PEI because we don't have a
- * means of determining which factored edges may be traversed by any
- * given PEI. (Maybe that may be more usual to have in the context
- * of a language with declared exception clauses...)
- *
- * Since we only deal with the most pessimistic exception case, this
- * means for each block we just determine a gen and kill set, along
- * with a subset of the latter set that is the locals that must be
- * killed before any PEI. (See killBeforePEI below.)
- *
- * Final note about types:
- *
- * Global DCE can change the types of locals in a way that spans
- * basic blocks. For a simple example, take the following code:
- *
- * // $foo :: Uninit here.
- * $foo = 12;
- * // ... code that breaks a block ...
- * $foo = 100;
- *
- * If the first store to $foo is removed, the type of $foo before
- * the SetL in the later block is now Uninit, instead of Int.
- *
- * This means that after calling global_dce on a function, the type
- * information in the block input states in the associated
- * FuncAnalysis can no longer be considered accurate.
- *
- * Moreover, since global DCE makes use of type information to
- * determine whether a store is dead, we need to be careful that
- * this never changes whether the assumptions used to perform DCE
- * were correct.
- *
- * This is ok right now: DCE only uses the types to figure out which
- * values can either have lifetimes extended or shortened without
- * visible side-effects, or which values may be refs (so we can't
- * omit stores to them). If we omit a store that changes the type
- * of a local globally, this means the new type cannot be different
- * with regard to these features (or we wouldn't have omitted it),
- * which means we won't have made different decisions elsewhere in
- * the algorithm based on the new type.
- *
- * Specifically: we will never omit a store where the old local type
- * was something that could've had side effects, and if a new value
- * is stored into a local where destroying it could have
- * side-effects, some point along the path to the function exit (if
- * nothing else, the RetC) will have added it to the gen set and the
- * store also won't be removable.
- *
- * In contrast, note that local DCE can not change types across
- * block boundaries.
- *
- */
- namespace {
- //////////////////////////////////////////////////////////////////////
- // Returns whether decrefing a type could run a destructor.
- bool couldRunDestructor(const Type& t) {
- // We could check for specialized objects to see if they don't
- // declare a user-defined destructor, but currently don't.
- return t.couldBe(TObj) || t.couldBe(TCArr) || t.couldBe(TRef);
- }
- // Returns whether a set on something containing type t could have
- // side-effects (running destuctors, or modifying arbitrary things via
- // a Ref).
- bool setCouldHaveSideEffects(const Type& t) {
- return t.couldBe(TObj) || t.couldBe(TCArr) || t.couldBe(TRef);
- }
- // Some reads could raise warnings and run arbitrary code.
- bool readCouldHaveSideEffects(const Type& t) {
- return t.couldBe(TUninit);
- }
- //////////////////////////////////////////////////////////////////////
- /*
- * Use information of a stack cell.
- */
- enum class Use {
- // Indicates that the cell is (unconditionally) not used.
- Not,
- // Indicates that the cell is (possibly) used.
- Used,
- /*
- * Indicates that the cell is only used if it was the last reference alive.
- * For instance, a PopC will call the destructor of the top-of-stack object
- * if it was the last reference alive, and this counts as an example of
- * 'UsedIfLastRef'.
- *
- * If the producer of the cell knows that it is not the last reference, then
- * it can treat Use::UsedIfLastRef as being equivalent to Use::Not.
- */
- UsedIfLastRef,
- };
- using InstrId = size_t;
- using InstrIdSet = std::set<InstrId>;
- using UseInfo = std::pair<Use,InstrIdSet>;
- //////////////////////////////////////////////////////////////////////
- struct DceState {
- borrowed_ptr<const php::Func> func;
- /*
- * Eval stack use information. Stacks slots are marked as being
- * needed or not needed. If they aren't needed, they carry a set of
- * instructions that must be removed if the instruction that
- * produces the stack value is also removable.
- */
- std::vector<UseInfo> stack;
- /*
- * Locals known to be live at a point in a DCE walk. This is used
- * when we're actually acting on information we discovered during
- * liveness analysis.
- */
- std::bitset<kMaxTrackedLocals> liveLocals;
- /*
- * These variable sets are used to compute the transfer function for
- * the global liveness analysis in global_dce.
- *
- * The gen set accumulates the set of variables in the block with
- * upward-exposed uses. The kill set is the set of variables the
- * block will re-define, ignoring exceptional control flow.
- *
- * The killBeforePEI set is the set of variables killed before a
- * PEI. Propagation of liveness needs to use this (always more
- * conservative) set instead of kill when crossing a factored exit
- * edge.
- */
- std::bitset<kMaxTrackedLocals> gen;
- std::bitset<kMaxTrackedLocals> kill;
- std::bitset<kMaxTrackedLocals> killBeforePEI;
- /*
- * Instructions marked in this set are dead. If any of them are
- * removed, however, they must all be removed, because of the need
- * to keep eval stack consumers and producers balanced.
- */
- boost::dynamic_bitset<> markedDead{};
- /*
- * The set of locals that were ever live in this block. (This
- * includes locals that were live going out of this block.) This
- * set is used by global DCE to remove locals that are completely
- * unused in the entire function.
- */
- std::bitset<kMaxTrackedLocals> usedLocals;
- };
- //////////////////////////////////////////////////////////////////////
- const char* show(Use u) {
- switch (u) {
- case Use::Not: return "0";
- case Use::Used: return "U";
- case Use::UsedIfLastRef: return "UL";
- }
- not_reached();
- }
- std::string show(const InstrIdSet& set) {
- using namespace folly::gen;
- return from(set)
- | eachTo<std::string>()
- | unsplit<std::string>(";")
- ;
- }
- std::string show(const UseInfo& ui) {
- return folly::format("{}@{}", show(ui.first), show(ui.second)).str();
- }
- std::string bits_string(borrowed_ptr<const php::Func> func,
- std::bitset<kMaxTrackedLocals> locs) {
- std::ostringstream out;
- if (func->locals.size() < kMaxTrackedLocals) {
- for (auto i = func->locals.size(); i-- > 0;) {
- out << (locs.test(i) ? '1' : '0');
- }
- } else {
- out << locs;
- }
- return out.str();
- }
- //////////////////////////////////////////////////////////////////////
- struct Env {
- DceState& dceState;
- InstrId id;
- const State& stateBefore;
- const StepFlags& flags;
- };
- void markSetDead(Env& env, const InstrIdSet& set) {
- env.dceState.markedDead[env.id] = 1;
- FTRACE(2, " marking {} {}\n", env.id, show(set));
- for (auto& i : set) env.dceState.markedDead[i] = 1;
- }
- void markDead(Env& env) {
- env.dceState.markedDead[env.id] = 1;
- FTRACE(2, " marking {}\n", env.id);
- }
- //////////////////////////////////////////////////////////////////////
- // eval stack
- void pop(Env& env, Use u, InstrIdSet set) {
- FTRACE(2, " pop({})\n", show(u));
- env.dceState.stack.emplace_back(u, std::move(set));
- }
- void pop(Env& env) { pop(env, Use::Used, InstrIdSet{}); }
- Type topT(Env& env, uint32_t idx = 0) {
- assert(idx < env.stateBefore.stack.size());
- return env.stateBefore.stack[env.stateBefore.stack.size() - idx - 1];
- }
- Type topC(Env& env, uint32_t idx = 0) {
- auto const t = topT(env, idx);
- assert(t.subtypeOf(TInitCell));
- return t;
- }
- void discard(Env& env) {
- pop(env, Use::Not, InstrIdSet{env.id});
- }
- bool allUnused() { return true; }
- template<class... Args>
- bool allUnused(const UseInfo& ui, Args&&... args) {
- return ui.first == Use::Not &&
- allUnused(std::forward<Args>(args)...);
- }
- void combineSets(InstrIdSet&) {}
- template<class... Args>
- void combineSets(InstrIdSet& accum, const UseInfo& ui, Args&&... args) {
- accum.insert(begin(ui.second), end(ui.second));
- combineSets(accum, std::forward<Args>(args)...);
- }
- // If all the supplied UseInfos represent unused stack slots, make a
- // pop that is considered unused. Otherwise pop as a Use::Used.
- template<class... Args>
- void popCond(Env& env, Args&&... args) {
- bool unused = allUnused(std::forward<Args>(args)...);
- if (!unused) return pop(env, Use::Used, InstrIdSet{});
- auto accum = InstrIdSet{env.id};
- combineSets(accum, std::forward<Args>(args)...);
- pop(env, Use::Not, accum);
- }
- /*
- * It may be ok to remove pops on objects with destructors in some scenarios
- * (where it won't change the observable point at which a destructor runs). We
- * could also look at the object type and see if it is known that it can't have
- * a user-defined destructor.
- *
- * For now, we mark the cell popped with a Use::UsedIfLastRef. This indicates
- * to the producer of the cell that the it is considered used if it could be
- * the last reference alive (in which case the destructor would be run on
- * Pop). If the producer knows that the cell is not the last reference (e.g. if
- * it is a Dup), then Use:UsedIfLastRef is equivalent to Use::Not.
- */
- void discardNonDtors(Env& env) {
- auto const t = topC(env);
- if (couldRunDestructor(t)) {
- return pop(env, Use::UsedIfLastRef, InstrIdSet{env.id});
- }
- discard(env);
- }
- UseInfo push(Env& env) {
- always_assert(!env.dceState.stack.empty());
- auto ret = env.dceState.stack.back();
- env.dceState.stack.pop_back();
- FTRACE(2, " {}@{} = push()\n", show(ret.first), show(ret.second));
- return ret;
- }
- void pushRemovable(Env& env) {
- auto const ui = push(env);
- switch (ui.first) {
- case Use::Not:
- markSetDead(env, ui.second);
- break;
- case Use::Used:
- case Use::UsedIfLastRef:
- break;
- }
- }
- //////////////////////////////////////////////////////////////////////
- // locals
- void addGenSet(Env& env, std::bitset<kMaxTrackedLocals> locs) {
- FTRACE(4, " conservative: {}\n", bits_string(env.dceState.func, locs));
- env.dceState.liveLocals |= locs;
- env.dceState.gen |= locs;
- env.dceState.kill &= ~locs;
- env.dceState.killBeforePEI &= ~locs;
- }
- void addGen(Env& env, uint32_t id) {
- FTRACE(2, " gen: {}\n", id);
- if (id >= kMaxTrackedLocals) return;
- env.dceState.liveLocals[id] = 1;
- env.dceState.gen[id] = 1;
- env.dceState.kill[id] = 0;
- env.dceState.killBeforePEI[id] = 0;
- }
- void addKill(Env& env, uint32_t id) {
- FTRACE(2, " kill: {}\n", id);
- if (id >= kMaxTrackedLocals) return;
- env.dceState.liveLocals[id] = 0;
- env.dceState.gen[id] = 0;
- env.dceState.kill[id] = 1;
- env.dceState.killBeforePEI[id] = 1;
- }
- bool isLive(Env& env, uint32_t id) {
- if (id >= kMaxTrackedLocals) {
- // Conservatively assume it's potentially live.
- return true;
- }
- return env.dceState.liveLocals[id];
- }
- Type locRaw(Env& env, borrowed_ptr<php::Local> loc) {
- return env.stateBefore.locals[loc->id];
- }
- void readDtorLocs(Env& env) {
- for (auto i = size_t{0}; i < env.stateBefore.locals.size(); ++i) {
- if (couldRunDestructor(env.stateBefore.locals[i])) {
- addGen(env, i);
- }
- }
- }
- //////////////////////////////////////////////////////////////////////
- /*
- * Note that the instructions with popConds are relying on the consumer of the
- * values they push to check whether lifetime changes can have side-effects.
- *
- * For example, in bytecode like this, assuming $x is an object with a
- * destructor:
- *
- * CGetL $x
- * UnsetL $x
- * // ...
- * PopC $x // dtor should be here.
- *
- * The PopC will decide it can't be eliminated, which prevents us from
- * eliminating the CGetL.
- */
- void dce(Env& env, const bc::PopC&) { discardNonDtors(env); }
- // For PopV and PopR currently we never know if can't run a
- // destructor.
- void dce(Env& env, const bc::PopA&) { discard(env); }
- void dce(Env& env, const bc::Int&) { pushRemovable(env); }
- void dce(Env& env, const bc::String&) { pushRemovable(env); }
- void dce(Env& env, const bc::Array&) { pushRemovable(env); }
- void dce(Env& env, const bc::Double&) { pushRemovable(env); }
- void dce(Env& env, const bc::True&) { pushRemovable(env); }
- void dce(Env& env, const bc::False&) { pushRemovable(env); }
- void dce(Env& env, const bc::Null&) { pushRemovable(env); }
- void dce(Env& env, const bc::NullUninit&) { pushRemovable(env); }
- void dce(Env& env, const bc::File&) { pushRemovable(env); }
- void dce(Env& env, const bc::Dir&) { pushRemovable(env); }
- void dce(Env& env, const bc::NameA&) { popCond(env, push(env)); }
- void dce(Env& env, const bc::NewArray&) { pushRemovable(env); }
- void dce(Env& env, const bc::NewCol&) { pushRemovable(env); }
- void dce(Env& env, const bc::AGetC&) { popCond(env, push(env)); }
- void dce(Env& env, const bc::Dup&) {
- auto const u1 = push(env);
- auto const u2 = push(env);
- // Dup pushes a cell that is guaranteed to be not the last reference.
- // So, it can be eliminated if the cell it pushes is used as either
- // Use::Not or Use::UsedIfLastRef.
- // The cell it pops can be marked Use::Not only if Dup itself
- // can be eliminated.
- switch (u1.first) {
- case Use::Not:
- case Use::UsedIfLastRef:
- // It is ok to eliminate the Dup even if its second output u2
- // is used, because eliminating the Dup still leaves the second
- // output u2 on stack.
- markSetDead(env, u1.second);
- switch (u2.first) {
- case Use::Not:
- pop(env, Use::Not, u2.second);
- break;
- case Use::Used:
- case Use::UsedIfLastRef:
- pop(env, Use::Used, InstrIdSet{});
- break;
- }
- break;
- case Use::Used:
- pop(env, Use::Used, InstrIdSet{});
- break;
- }
- }
- void dce(Env& env, const bc::CGetL& op) {
- auto const ty = locRaw(env, op.loc1);
- addGen(env, op.loc1->id);
- if (readCouldHaveSideEffects(ty)) {
- push(env);
- } else {
- pushRemovable(env);
- }
- }
- void dce(Env& env, const bc::CGetL2& op) {
- auto const ty = locRaw(env, op.loc1);
- addGen(env, op.loc1->id);
- auto const u1 = push(env);
- auto const u2 = push(env);
- if (readCouldHaveSideEffects(ty)) {
- pop(env);
- } else {
- popCond(env, u1, u2);
- }
- }
- void dce(Env& env, const bc::CGetL3& op) {
- auto const ty = locRaw(env, op.loc1);
- addGen(env, op.loc1->id);
- auto const u1 = push(env);
- auto const u2 = push(env);
- auto const u3 = push(env);
- if (readCouldHaveSideEffects(ty)) {
- pop(env);
- pop(env);
- } else {
- popCond(env, u1, u2, u3);
- popCond(env, u1, u2, u3);
- }
- }
- void dce(Env& env, const bc::RetC&) { pop(env); readDtorLocs(env); }
- void dce(Env& env, const bc::Throw&) { pop(env); readDtorLocs(env); }
- void dce(Env& env, const bc::Fatal&) { pop(env); readDtorLocs(env); }
- void dce(Env& env, const bc::Exit&) { push(env); pop(env); readDtorLocs(env); }
- void dce(Env& env, const bc::SetL& op) {
- auto const oldTy = locRaw(env, op.loc1);
- auto const effects = setCouldHaveSideEffects(oldTy);
- if (!isLive(env, op.loc1->id) && !effects) return markDead(env);
- push(env);
- pop(env);
- if (!effects) addKill(env, op.loc1->id);
- if (effects) addGen(env, op.loc1->id);
- }
- /*
- * IncDecL is a read-modify-write: can be removed if the local isn't live, the
- * set can't have side effects, and no one reads the value it pushes. If the
- * instruction is not dead, always add the local to the set of upward exposed
- * uses.
- */
- void dce(Env& env, const bc::IncDecL& op) {
- auto const oldTy = locRaw(env, op.loc1);
- auto const effects = setCouldHaveSideEffects(oldTy) ||
- readCouldHaveSideEffects(oldTy);
- auto const u1 = push(env);
- if (!isLive(env, op.loc1->id) && !effects && allUnused(u1)) {
- return markSetDead(env, u1.second);
- }
- addGen(env, op.loc1->id);
- }
- /*
- * SetOpL is like IncDecL, but with the complication that we don't know if we
- * can mark it dead when visiting it, because it is going to pop an input but
- * unlike SetL doesn't push the value it popped. For the current scheme we
- * just add the local to gen even if we're doing a removable push, which is
- * correct but could definitely fail to eliminate some earlier stores.
- */
- void dce(Env& env, const bc::SetOpL& op) {
- auto const oldTy = locRaw(env, op.loc1);
- auto const effects = setCouldHaveSideEffects(oldTy) ||
- readCouldHaveSideEffects(oldTy);
- if (!isLive(env, op.loc1->id) && !effects) {
- popCond(env, push(env));
- } else {
- push(env);
- pop(env);
- }
- addGen(env, op.loc1->id);
- }
- /*
- * Default implementation is conservative: assume we use all of our
- * inputs, and can't be removed even if our output is unused.
- *
- * We also assume all the locals in the mayReadLocalSet must be
- * added to the live local set, and don't remove anything from it.
- */
- template<class Op>
- void dce(Env& env, const Op& op) {
- addGenSet(env, env.flags.mayReadLocalSet);
- env.dceState.liveLocals |= env.flags.mayReadLocalSet;
- for (auto i = uint32_t{0}; i < op.numPush(); ++i) {
- push(env);
- }
- for (auto i = uint32_t{0}; i < op.numPop(); ++i) {
- pop(env, Use::Used, InstrIdSet{});
- }
- }
- void dispatch_dce(Env& env, const Bytecode& op) {
- #define O(opcode, ...) case Op::opcode: dce(env, op.opcode); return;
- switch (op.op) { OPCODES }
- #undef O
- not_reached();
- }
- //////////////////////////////////////////////////////////////////////
- folly::Optional<DceState>
- dce_visit(const Index& index,
- Context const ctx,
- borrowed_ptr<const php::Block> const blk,
- const State& stateIn,
- std::bitset<kMaxTrackedLocals> liveOut,
- std::bitset<kMaxTrackedLocals> liveOutExn) {
- if (!stateIn.initialized) {
- /*
- * Skip unreachable blocks.
- *
- * For DCE analysis it is ok to assume the transfer function is
- * the identity on unreachable blocks (i.e. gen and kill sets are
- * empty). For optimize, we don't need to bother doing anything
- * to these---another pass is responsible for removing completely
- * unreachable blocks.
- */
- return folly::none;
- }
- auto const states = locally_propagated_states(index, ctx, blk, stateIn);
- auto dceState = DceState{};
- dceState.func = ctx.func;
- dceState.markedDead.resize(blk->hhbcs.size());
- dceState.liveLocals = liveOut;
- dceState.stack.resize(states.back().first.stack.size());
- dceState.usedLocals = liveOut;
- for (auto& s : dceState.stack) {
- s = UseInfo { Use::Used, InstrIdSet{} };
- }
- for (auto idx = blk->hhbcs.size(); idx-- > 0;) {
- auto const& op = blk->hhbcs[idx];
- FTRACE(2, " == #{} {}\n", idx, show(op));
- auto visit_env = Env {
- dceState,
- idx,
- states[idx].first,
- states[idx].second
- };
- dispatch_dce(visit_env, op);
- /*
- * When we see a PEI, we need to start over on the killBeforePEI
- * set, and the local-liveness must take into account the fact
- * that we could take an exception edge here (or'ing in the
- * liveOutExn set).
- */
- if (states[idx].second.wasPEI) {
- FTRACE(2, " <-- exceptions\n");
- dceState.liveLocals |= liveOutExn;
- dceState.killBeforePEI.reset();
- }
- dceState.usedLocals |= dceState.liveLocals;
- FTRACE(4, " dce stack: {}\n",
- [&] {
- using namespace folly::gen;
- return from(dceState.stack)
- | map([&] (const UseInfo& ui) { return show(ui); })
- | unsplit<std::string>(" ");
- }()
- );
- FTRACE(4, " interp stack: {}\n",
- [&] {
- using namespace folly::gen;
- return from(states[idx].first.stack)
- | map([&] (const Type& t) { return show(t); })
- | unsplit<std::string>(" ");
- }()
- );
- // We're now at the state before this instruction, so the stack
- // sizes must line up.
- assert(dceState.stack.size() == states[idx].first.stack.size());
- }
- return dceState;
- }
- struct DceAnalysis {
- std::bitset<kMaxTrackedLocals> gen;
- std::bitset<kMaxTrackedLocals> kill;
- std::bitset<kMaxTrackedLocals> killExn;
- };
- DceAnalysis analyze_dce(const Index& index,
- Context const ctx,
- borrowed_ptr<php::Block> const blk,
- const State& stateIn) {
- // During this analysis pass, we have to assume everything could be
- // live out, so we set allLive here. (Later we'll determine the
- // real liveOut sets.)
- auto allLive = std::bitset<kMaxTrackedLocals>{};
- allLive.set();
- if (auto dceState = dce_visit(index, ctx, blk, stateIn, allLive, allLive)) {
- return DceAnalysis {
- dceState->gen,
- dceState->kill,
- dceState->killBeforePEI
- };
- }
- return DceAnalysis {};
- }
- std::bitset<kMaxTrackedLocals>
- optimize_dce(const Index& index,
- Context const ctx,
- borrowed_ptr<php::Block> const blk,
- const State& stateIn,
- std::bitset<kMaxTrackedLocals> liveOut,
- std::bitset<kMaxTrackedLocals> liveOutExn) {
- auto const dceState = dce_visit(index, ctx, blk,
- stateIn, liveOut, liveOutExn);
- if (!dceState) return std::bitset<kMaxTrackedLocals>{};
- // Remove all instructions that were marked dead, and replace
- // instructions that can be replaced with pops but aren't dead.
- for (auto idx = blk->hhbcs.size(); idx-- > 0;) {
- if (!dceState->markedDead.test(idx)) continue;
- blk->hhbcs.erase(begin(blk->hhbcs) + idx);
- }
- // Blocks must be non-empty. Make sure we don't change that.
- if (blk->hhbcs.empty()) {
- blk->hhbcs.push_back(bc::Nop {});
- }
- return dceState->usedLocals;
- }
- //////////////////////////////////////////////////////////////////////
- void remove_unused_locals(Context const ctx,
- std::bitset<kMaxTrackedLocals> usedLocals) {
- if (!options.RemoveUnusedLocals) return;
- auto const func = ctx.func;
- /*
- * Removing unused locals in closures requires checking which ones
- * are captured variables so we can remove the relevant properties,
- * and then we'd have to mutate the CreateCl callsite, so we don't
- * bother for now.
- *
- * Note: many closure bodies have unused $this local, because of
- * some emitter quirk, so this might be worthwhile.
- */
- if (func->isClosureBody) return;
- func->locals.erase(
- std::remove_if(
- begin(func->locals) + func->params.size(),
- end(func->locals),
- [&] (const std::unique_ptr<php::Local>& l) {
- if (l->id < kMaxTrackedLocals && !usedLocals.test(l->id)) {
- FTRACE(2, " removing: {}\n", local_string(borrow(l)));
- return true;
- }
- return false;
- }
- ),
- end(func->locals)
- );
- // Fixup local ids, in case we removed any.
- for (auto i = uint32_t{0}; i < func->locals.size(); ++i) {
- func->locals[i]->id = i;
- }
- }
- //////////////////////////////////////////////////////////////////////
- }
- void local_dce(const Index& index,
- const FuncAnalysis& ainfo,
- borrowed_ptr<php::Block> const blk,
- const State& stateIn) {
- Trace::Bump bumper{Trace::hhbbc_dce, kSystemLibBump,
- is_systemlib_part(*ainfo.ctx.unit)};
- // For local DCE, we have to assume all variables are in the
- // live-out set for the block.
- auto allLive = std::bitset<kMaxTrackedLocals>();
- allLive.set();
- optimize_dce(index, ainfo.ctx, blk, stateIn, allLive, allLive);
- }
- //////////////////////////////////////////////////////////////////////
- void global_dce(const Index& index, const FuncAnalysis& ai) {
- Trace::Bump bumper{Trace::hhbbc_dce, kSystemLibBump,
- is_systemlib_part(*ai.ctx.unit)};
- auto rpoId = [&] (borrowed_ptr<php::Block> blk) {
- return ai.bdata[blk->id].rpoId;
- };
- FTRACE(1, "|---- global DCE analyze ({})\n", show(ai.ctx));
- FTRACE(2, "{}", [&] {
- using namespace folly::gen;
- auto i = uint32_t{0};
- return from(ai.ctx.func->locals)
- | mapped(
- [&] (const std::unique_ptr<php::Local>& l) {
- return folly::sformat(" {} {}\n", i++, local_string(borrow(l)));
- })
- | unsplit<std::string>("");
- }());
- /*
- * Create a DceAnalysis for each block, indexed by rpo id.
- *
- * Here we want to pre-compute the transfer function for each block,
- * so we don't need to visit each instruction repeatedly during the
- * fixed point computation. The transfer function is a function of
- * the set of locals read and killed in the block, and does not
- * depend on the final live out state, so we can compute it here.
- */
- auto blockAnalysis = std::vector<DceAnalysis>{};
- for (auto& b : ai.rpoBlocks) {
- FTRACE(2, "block #{}\n", b->id);
- auto const dinfo = analyze_dce(
- index,
- ai.ctx,
- b,
- ai.bdata[b->id].stateIn
- );
- blockAnalysis.push_back(dinfo);
- }
- /*
- * States for each block, indexed by RPO id.
- *
- * The liveOut state is the union of liveIn states of each normal
- * successor, and liveOutExn is the union of liveIn states of each
- * exceptional successor.
- */
- struct BlockState {
- std::bitset<kMaxTrackedLocals> liveOut;
- std::bitset<kMaxTrackedLocals> liveOutExn;
- };
- std::vector<BlockState> blockStates(ai.rpoBlocks.size());
- /*
- * Set of block reverse post order ids that still need to be
- * visited. This is ordered by std::greater, so we'll generally
- * visit blocks before their predecessors. (The algorithm doesn't
- * need this for correctness, but it might make it iterate less.)
- *
- * Every block must be visited at least once, so we throw them all
- * in to start.
- */
- auto incompleteQ = dataflow_worklist<uint32_t,std::less<uint32_t>>(
- ai.rpoBlocks.size()
- );
- for (auto& b : ai.rpoBlocks) incompleteQ.push(rpoId(b));
- auto const normalPreds = computeNormalPreds(ai.rpoBlocks);
- auto const factoredPreds = computeFactoredPreds(ai.rpoBlocks);
- /*
- * Iterate on live out states until we reach a fixed point.
- *
- * This algorithm treats the exceptional live-out states differently
- * from the live-out states during normal control flow. The
- * liveOutExn sets only take part in the liveIn computation when the
- * block has factored exits.
- */
- while (!incompleteQ.empty()) {
- auto const blk = ai.rpoBlocks[incompleteQ.pop()];
- FTRACE(2, "block #{}\n", blk->id);
- auto const liveOut = blockStates[rpoId(blk)].liveOut;
- auto const liveOutExn = blockStates[rpoId(blk)].liveOutExn;
- auto const transfer = blockAnalysis[rpoId(blk)];
- auto const liveIn = transfer.gen | (liveOut & ~transfer.kill)
- | (liveOutExn & ~transfer.killExn);
- FTRACE(2, "live out : {}\n"
- "out exn : {}\n"
- "gen : {}\n"
- "kill : {}\n"
- "kill exn : {}\n"
- "live in : {}\n",
- bits_string(ai.ctx.func, liveOut),
- bits_string(ai.ctx.func, liveOutExn),
- bits_string(ai.ctx.func, transfer.gen),
- bits_string(ai.ctx.func, transfer.kill),
- bits_string(ai.ctx.func, transfer.killExn),
- bits_string(ai.ctx.func, liveIn));
- // Merge the liveIn into the liveOut of each normal predecessor.
- // If the set changes, reschedule that predecessor.
- for (auto& pred : normalPreds[blk->id]) {
- FTRACE(2, " -> {}\n", pred->id);
- auto& predState = blockStates[rpoId(pred)].liveOut;
- auto const oldPredState = predState;
- predState |= liveIn;
- if (predState != oldPredState) {
- incompleteQ.push(rpoId(pred));
- }
- }
- // Merge the liveIn into the liveOutExn state for each exceptional
- // precessor. The liveIn computation also depends on the
- // liveOutExn state, so again reschedule if it changes.
- for (auto& pred : factoredPreds[blk->id]) {
- FTRACE(2, " => {}\n", pred->id);
- auto& predState = blockStates[rpoId(pred)].liveOutExn;
- auto const oldPredState = predState;
- predState |= liveIn;
- if (predState != oldPredState) {
- incompleteQ.push(rpoId(pred));
- }
- }
- }
- /*
- * Now that we're at a fixed point, use the propagated states to
- * remove instructions that don't need to be there.
- */
- FTRACE(1, "|---- global DCE optimize ({})\n", show(ai.ctx));
- std::bitset<kMaxTrackedLocals> usedLocals;
- for (auto& b : ai.rpoBlocks) {
- FTRACE(2, "block #{}\n", b->id);
- usedLocals |= optimize_dce(
- index,
- ai.ctx,
- b,
- ai.bdata[b->id].stateIn,
- blockStates[rpoId(b)].liveOut,
- blockStates[rpoId(b)].liveOutExn
- );
- }
- FTRACE(1, " used locals: {}\n", bits_string(ai.ctx.func, usedLocals));
- remove_unused_locals(ai.ctx, usedLocals);
- }
- //////////////////////////////////////////////////////////////////////
- void remove_unreachable_blocks(const Index& index, const FuncAnalysis& ainfo) {
- boost::dynamic_bitset<> reachable(ainfo.ctx.func->nextBlockId);
- for (auto& blk : ainfo.rpoBlocks) {
- reachable[blk->id] = ainfo.bdata[blk->id].stateIn.initialized;
- if (reachable[blk->id]) continue;
- auto const srcLoc = blk->hhbcs.front().srcLoc;
- blk->hhbcs = {
- bc_with_loc(srcLoc, bc::String { s_unreachable.get() }),
- bc_with_loc(srcLoc, bc::Fatal { FatalOp::Runtime })
- };
- blk->fallthrough = nullptr;
- }
- if (!options.RemoveDeadBlocks) return;
- for (auto& blk : ainfo.rpoBlocks) {
- auto reachableTargets = false;
- forEachTakenEdge(blk->hhbcs.back(), [&] (php::Block& target) {
- if (reachable[target.id]) reachableTargets = true;
- });
- if (reachableTargets) continue;
- switch (blk->hhbcs.back().op) {
- case Op::JmpNZ:
- case Op::JmpZ:
- blk->hhbcs.back() = bc_with_loc(blk->hhbcs.back().srcLoc, bc::PopC {});
- break;
- default:
- break;
- }
- }
- }
- //////////////////////////////////////////////////////////////////////
- }}