// Copyright (c) 2017 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #ifndef LIBSPIRV_OPT_PROPAGATOR_H_ #define LIBSPIRV_OPT_PROPAGATOR_H_ #include #include #include #include #include #include #include "ir_context.h" #include "module.h" namespace spvtools { namespace opt { // Represents a CFG control edge. struct Edge { explicit Edge(ir::BasicBlock* b1, ir::BasicBlock* b2) : source(b1), dest(b2) {} ir::BasicBlock* source; ir::BasicBlock* dest; bool operator<(const Edge& o) const { return std::make_pair(source->id(), dest->id()) < std::make_pair(o.source->id(), o.dest->id()); } }; // This class implements a generic value propagation algorithm based on the // conditional constant propagation algorithm proposed in // // Constant propagation with conditional branches, // Wegman and Zadeck, ACM TOPLAS 13(2):181-210. // // A Propagation Engine for GCC // Diego Novillo, GCC Summit 2005 // http://ols.fedoraproject.org/GCC/Reprints-2005/novillo-Reprint.pdf // // The purpose of this implementation is to act as a common framework for any // transformation that needs to propagate values from statements producing new // values to statements using those values. Simulation proceeds as follows: // // 1- Initially, all edges of the CFG are marked not executable and the CFG // worklist is seeded with all the statements in the entry basic block. // // 2- Every instruction I is simulated by calling a pass-provided function // |visit_fn|. This function is responsible for three things: // // (a) Keep a value table of interesting values. This table maps SSA IDs to // their values. For instance, when implementing constant propagation, // given a store operation 'OpStore %f %int_3', |visit_fn| should assign // the value 3 to the table slot for %f. // // In general, |visit_fn| will need to use the value table to replace its // operands, fold the result and decide whether a new value needs to be // stored in the table. |visit_fn| should only create a new mapping in // the value table if all the operands in the instruction are known and // present in the value table. // // (b) Return a status indicator to direct the propagator logic. Once the // instruction is simulated, the propagator needs to know whether this // instruction produced something interesting. This is indicated via // |visit_fn|'s return value: // // SSAPropagator::kNotInteresting: Instruction I produces nothing of // interest and does not affect any of the work lists. The // propagator will visit the statement again if any of its operands // produce an interesting value in the future. // // |visit_fn| should always return this value when it is not sure // whether the instruction will produce an interesting value in the // future or not. For instance, for constant propagation, an OpIAdd // instruction may produce a constant if its two operands are // constant, but the first time we visit the instruction, we still // may not have its operands in the value table. // // SSAPropagator::kVarying: The value produced by I cannot be determined // at compile time. Further simulation of I is not required. The // propagator will not visit this instruction again. Additionally, // the propagator will add all the instructions at the end of SSA // def-use edges to be simulated again. // // If I is a basic block terminator, it will mark all outgoing edges // as executable so they are traversed one more time. Eventually // the kVarying attribute will be spread out to all the data and // control dependents for I. // // It is important for propagation to use kVarying as a bottom value // for the propagation lattice. It should never be possible for an // instruction to return kVarying once and kInteresting on a second // visit. Otherwise, propagation would not stabilize. // // SSAPropagator::kInteresting: Instruction I produces a value that can // be computed at compile time. In this case, |visit_fn| should // create a new mapping between I's result ID and the produced // value. Much like the kNotInteresting case, the propagator will // visit this instruction again if any of its operands changes. // This is useful when the statement changes from one interesting // state to another. // // (c) For conditional branches, |visit_fn| may decide which edge to take out // of I's basic block. For example, if the operand for an OpSwitch is // known to take a specific constant value, |visit_fn| should figure out // the destination basic block and pass it back by setting the second // argument to |visit_fn|. // // At the end of propagation, values in the value table are guaranteed to be // stable and can be replaced in the IR. // // 3- The propagator keeps two work queues. Instructions are only added to // these queues if they produce an interesting or varying value. None of this // should be handled by |visit_fn|. The propagator keeps track of this // automatically (see SSAPropagator::Simulate for implementation). // // CFG blocks: contains the queue of blocks to be simulated. // Blocks are added to this queue if their incoming edges are // executable. // // SSA Edges: An SSA edge is a def-use edge between a value-producing // instruction and its use instruction. The SSA edges list // contains the statements at the end of a def-use edge that need // to be re-visited when an instruction produces a kVarying or // kInteresting result. // // 4- Simulation terminates when all work queues are drained. // // // EXAMPLE: Basic constant store propagator. // // Suppose we want to propagate all constant assignments of the form "OpStore // %id %cst" where "%id" is some variable and "%cst" an OpConstant. The // following code builds a table |values| where every id that was assigned a // constant value is mapped to the constant value it was assigned. // // auto ctx = spvtools::BuildModule(...); // std::map values; // const auto visit_fn = [&ctx, &values](ir::Instruction* instr, // ir::BasicBlock** dest_bb) { // if (instr->opcode() == SpvOpStore) { // uint32_t rhs_id = instr->GetSingleWordOperand(1); // ir::Instruction* rhs_def = ctx->get_def_use_mgr()->GetDef(rhs_id); // if (rhs_def->opcode() == SpvOpConstant) { // uint32_t val = rhs_def->GetSingleWordOperand(2); // values[rhs_id] = val; // return opt::SSAPropagator::kInteresting; // } // } // return opt::SSAPropagator::kVarying; // }; // opt::SSAPropagator propagator(ctx.get(), &cfg, visit_fn); // propagator.Run(&fn); // // Given the code: // // %int_4 = OpConstant %int 4 // %int_3 = OpConstant %int 3 // %int_1 = OpConstant %int 1 // OpStore %x %int_4 // OpStore %y %int_3 // OpStore %z %int_1 // // After SSAPropagator::Run returns, the |values| map will contain the entries: // values[%x] = 4, values[%y] = 3, and, values[%z] = 1. class SSAPropagator { public: // Lattice values used for propagation. See class documentation for // a description. enum PropStatus { kNotInteresting, kInteresting, kVarying }; using VisitFunction = std::function; SSAPropagator(ir::IRContext* context, const VisitFunction& visit_fn) : ctx_(context), visit_fn_(visit_fn) {} // Run the propagator on function |fn|. Returns true if changes were made to // the function. Otherwise, it returns false. bool Run(ir::Function* fn); private: // Initialize processing. void Initialize(ir::Function* fn); // Simulate the execution |block| by calling |visit_fn_| on every instruction // in it. bool Simulate(ir::BasicBlock* block); // Simulate the execution of |instr| by replacing all the known values in // every operand and determining whether the result is interesting for // propagation. This invokes the callback function |visit_fn_| to determine // the value computed by |instr|. bool Simulate(ir::Instruction* instr); // Returns true if |instr| should be simulated again. bool ShouldSimulateAgain(ir::Instruction* instr) const { return do_not_simulate_.find(instr) == do_not_simulate_.end(); } // Add |instr| to the set of instructions not to simulate again. void DontSimulateAgain(ir::Instruction* instr) { do_not_simulate_.insert(instr); } // Returns true if |block| has been simulated already. bool BlockHasBeenSimulated(ir::BasicBlock* block) { return simulated_blocks_.find(block) != simulated_blocks_.end(); } // Marks block |block| as simulated. void MarkBlockSimulated(ir::BasicBlock* block) { simulated_blocks_.insert(block); } // Marks |edge| as executable. Returns false if the edge was already marked // as executable. bool MarkEdgeExecutable(const Edge& edge) { return executable_edges_.insert(edge).second; } // Returns true if |edge| has been marked as executable. bool IsEdgeExecutable(const Edge& edge) { return executable_edges_.find(edge) != executable_edges_.end(); } // Returns a pointer to the def-use manager for |ctx_|. analysis::DefUseManager* get_def_use_mgr() const { return ctx_->get_def_use_mgr(); } // If the CFG edge |e| has not been executed, add its destination block to the // work list. void AddControlEdge(const Edge& e); // Add all the instructions that use |id| to the SSA edges work list. void AddSSAEdges(uint32_t id); // IR context to use. ir::IRContext* ctx_; // Function that visits instructions during simulation. The output of this // function is used to determine if the simulated instruction produced a value // interesting for propagation. The function is responsible for keeping // track of interesting values by storing them in some user-provided map. VisitFunction visit_fn_; // SSA def-use edges to traverse. Each entry is a destination statement for an // SSA def-use edge as returned by |def_use_manager_|. std::queue ssa_edge_uses_; // Blocks to simulate. std::queue blocks_; // Blocks simulated during propagation. std::unordered_set simulated_blocks_; // Set of instructions that should not be simulated again because they have // been found to be in the kVarying state. std::unordered_set do_not_simulate_; // Map between a basic block and its predecessor edges. // TODO(dnovillo): Move this to ir::CFG and always build them. Alternately, // move it to IRContext and build CFG preds/succs on-demand. std::unordered_map> bb_preds_; // Map between a basic block and its successor edges. // TODO(dnovillo): Move this to ir::CFG and always build them. Alternately, // move it to IRContext and build CFG preds/succs on-demand. std::unordered_map> bb_succs_; // Set of executable CFG edges. std::set executable_edges_; }; } // namespace opt } // namespace spvtools #endif // LIBSPIRV_OPT_PROPAGATOR_H_