mirror of
https://github.com/KhronosGroup/SPIRV-Tools
synced 2024-11-27 13:50:07 +00:00
0f335cf87e
GCD MIV test as described in Chapter 3 of "Optimizing Compilers for Modern Architectures: A Dependence-Based Approach" by Randy Allen, and Ken Kennedy. Delta test as described in Figure 3 of "Practical Dependence Testing" by Gina Goff, Ken Kennedy, and Chau-Wen Tseng from PLDI '91.
563 lines
20 KiB
C++
563 lines
20 KiB
C++
// Copyright (c) 2018 Google LLC.
|
|
//
|
|
// 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 SOURCE_OPT_LOOP_DEPENDENCE_H_
|
|
#define SOURCE_OPT_LOOP_DEPENDENCE_H_
|
|
|
|
#include <algorithm>
|
|
#include <cstdint>
|
|
#include <list>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <ostream>
|
|
#include <set>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "opt/instruction.h"
|
|
#include "opt/ir_context.h"
|
|
#include "opt/loop_descriptor.h"
|
|
#include "opt/scalar_analysis.h"
|
|
|
|
namespace spvtools {
|
|
namespace opt {
|
|
|
|
// Stores information about dependence between a load and a store wrt a single
|
|
// loop in a loop nest.
|
|
// DependenceInformation
|
|
// * UNKNOWN if no dependence information can be gathered or is gathered
|
|
// for it.
|
|
// * DIRECTION if a dependence direction could be found, but not a
|
|
// distance.
|
|
// * DISTANCE if a dependence distance could be found.
|
|
// * PEEL if peeling either the first or last iteration will break
|
|
// dependence between the given load and store.
|
|
// * IRRELEVANT if it has no effect on the dependence between the given
|
|
// load and store.
|
|
//
|
|
// If peel_first == true, the analysis has found that peeling the first
|
|
// iteration of this loop will break dependence.
|
|
//
|
|
// If peel_last == true, the analysis has found that peeling the last iteration
|
|
// of this loop will break dependence.
|
|
class DistanceEntry {
|
|
public:
|
|
enum DependenceInformation {
|
|
UNKNOWN = 0,
|
|
DIRECTION = 1,
|
|
DISTANCE = 2,
|
|
PEEL = 3,
|
|
IRRELEVANT = 4,
|
|
POINT = 5
|
|
};
|
|
enum Directions {
|
|
NONE = 0,
|
|
LT = 1,
|
|
EQ = 2,
|
|
LE = 3,
|
|
GT = 4,
|
|
NE = 5,
|
|
GE = 6,
|
|
ALL = 7
|
|
};
|
|
DependenceInformation dependence_information;
|
|
Directions direction;
|
|
int64_t distance;
|
|
bool peel_first;
|
|
bool peel_last;
|
|
int64_t point_x;
|
|
int64_t point_y;
|
|
|
|
DistanceEntry()
|
|
: dependence_information(DependenceInformation::UNKNOWN),
|
|
direction(Directions::ALL),
|
|
distance(0),
|
|
peel_first(false),
|
|
peel_last(false),
|
|
point_x(0),
|
|
point_y(0) {}
|
|
|
|
explicit DistanceEntry(Directions direction_)
|
|
: dependence_information(DependenceInformation::DIRECTION),
|
|
direction(direction_),
|
|
distance(0),
|
|
peel_first(false),
|
|
peel_last(false),
|
|
point_x(0),
|
|
point_y(0) {}
|
|
|
|
DistanceEntry(Directions direction_, int64_t distance_)
|
|
: dependence_information(DependenceInformation::DISTANCE),
|
|
direction(direction_),
|
|
distance(distance_),
|
|
peel_first(false),
|
|
peel_last(false),
|
|
point_x(0),
|
|
point_y(0) {}
|
|
|
|
DistanceEntry(int64_t x, int64_t y)
|
|
: dependence_information(DependenceInformation::POINT),
|
|
direction(Directions::ALL),
|
|
distance(0),
|
|
peel_first(false),
|
|
peel_last(false),
|
|
point_x(x),
|
|
point_y(y) {}
|
|
|
|
bool operator==(const DistanceEntry& rhs) const {
|
|
return direction == rhs.direction && peel_first == rhs.peel_first &&
|
|
peel_last == rhs.peel_last && distance == rhs.distance &&
|
|
point_x == rhs.point_x && point_y == rhs.point_y;
|
|
}
|
|
|
|
bool operator!=(const DistanceEntry& rhs) const { return !(*this == rhs); }
|
|
};
|
|
|
|
// Stores a vector of DistanceEntrys, one per loop in the analysis.
|
|
// A DistanceVector holds all of the information gathered in a dependence
|
|
// analysis wrt the loops stored in the LoopDependenceAnalysis performing the
|
|
// analysis.
|
|
class DistanceVector {
|
|
public:
|
|
explicit DistanceVector(size_t size) : entries(size, DistanceEntry{}) {}
|
|
|
|
explicit DistanceVector(std::vector<DistanceEntry> entries_)
|
|
: entries(entries_) {}
|
|
|
|
DistanceEntry& GetEntry(size_t index) { return entries[index]; }
|
|
const DistanceEntry& GetEntry(size_t index) const { return entries[index]; }
|
|
|
|
std::vector<DistanceEntry>& GetEntries() { return entries; }
|
|
const std::vector<DistanceEntry>& GetEntries() const { return entries; }
|
|
|
|
bool operator==(const DistanceVector& rhs) const {
|
|
if (entries.size() != rhs.entries.size()) {
|
|
return false;
|
|
}
|
|
for (size_t i = 0; i < entries.size(); ++i) {
|
|
if (entries[i] != rhs.entries[i]) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
bool operator!=(const DistanceVector& rhs) const { return !(*this == rhs); }
|
|
|
|
private:
|
|
std::vector<DistanceEntry> entries;
|
|
};
|
|
|
|
class DependenceLine;
|
|
class DependenceDistance;
|
|
class DependencePoint;
|
|
class DependenceNone;
|
|
class DependenceEmpty;
|
|
|
|
class Constraint {
|
|
public:
|
|
explicit Constraint(const ir::Loop* loop) : loop_(loop) {}
|
|
enum ConstraintType { Line, Distance, Point, None, Empty };
|
|
|
|
virtual ConstraintType GetType() const = 0;
|
|
|
|
virtual ~Constraint() {}
|
|
|
|
// Get the loop this constraint belongs to.
|
|
const ir::Loop* GetLoop() const { return loop_; }
|
|
|
|
bool operator==(const Constraint& other) const;
|
|
|
|
bool operator!=(const Constraint& other) const;
|
|
|
|
#define DeclareCastMethod(target) \
|
|
virtual target* As##target() { return nullptr; } \
|
|
virtual const target* As##target() const { return nullptr; }
|
|
DeclareCastMethod(DependenceLine);
|
|
DeclareCastMethod(DependenceDistance);
|
|
DeclareCastMethod(DependencePoint);
|
|
DeclareCastMethod(DependenceNone);
|
|
DeclareCastMethod(DependenceEmpty);
|
|
#undef DeclareCastMethod
|
|
|
|
protected:
|
|
const ir::Loop* loop_;
|
|
};
|
|
|
|
class DependenceLine : public Constraint {
|
|
public:
|
|
DependenceLine(SENode* a, SENode* b, SENode* c, const ir::Loop* loop)
|
|
: Constraint(loop), a_(a), b_(b), c_(c) {}
|
|
|
|
ConstraintType GetType() const final { return Line; }
|
|
|
|
DependenceLine* AsDependenceLine() final { return this; }
|
|
const DependenceLine* AsDependenceLine() const final { return this; }
|
|
|
|
SENode* GetA() const { return a_; }
|
|
SENode* GetB() const { return b_; }
|
|
SENode* GetC() const { return c_; }
|
|
|
|
private:
|
|
SENode* a_;
|
|
SENode* b_;
|
|
SENode* c_;
|
|
};
|
|
|
|
class DependenceDistance : public Constraint {
|
|
public:
|
|
DependenceDistance(SENode* distance, const ir::Loop* loop)
|
|
: Constraint(loop), distance_(distance) {}
|
|
|
|
ConstraintType GetType() const final { return Distance; }
|
|
|
|
DependenceDistance* AsDependenceDistance() final { return this; }
|
|
const DependenceDistance* AsDependenceDistance() const final { return this; }
|
|
|
|
SENode* GetDistance() const { return distance_; }
|
|
|
|
private:
|
|
SENode* distance_;
|
|
};
|
|
|
|
class DependencePoint : public Constraint {
|
|
public:
|
|
DependencePoint(SENode* source, SENode* destination, const ir::Loop* loop)
|
|
: Constraint(loop), source_(source), destination_(destination) {}
|
|
|
|
ConstraintType GetType() const final { return Point; }
|
|
|
|
DependencePoint* AsDependencePoint() final { return this; }
|
|
const DependencePoint* AsDependencePoint() const final { return this; }
|
|
|
|
SENode* GetSource() const { return source_; }
|
|
SENode* GetDestination() const { return destination_; }
|
|
|
|
private:
|
|
SENode* source_;
|
|
SENode* destination_;
|
|
};
|
|
|
|
class DependenceNone : public Constraint {
|
|
public:
|
|
DependenceNone() : Constraint(nullptr) {}
|
|
ConstraintType GetType() const final { return None; }
|
|
|
|
DependenceNone* AsDependenceNone() final { return this; }
|
|
const DependenceNone* AsDependenceNone() const final { return this; }
|
|
};
|
|
|
|
class DependenceEmpty : public Constraint {
|
|
public:
|
|
DependenceEmpty() : Constraint(nullptr) {}
|
|
ConstraintType GetType() const final { return Empty; }
|
|
|
|
DependenceEmpty* AsDependenceEmpty() final { return this; }
|
|
const DependenceEmpty* AsDependenceEmpty() const final { return this; }
|
|
};
|
|
|
|
// Provides dependence information between a store instruction and a load
|
|
// instruction inside the same loop in a loop nest.
|
|
//
|
|
// The analysis can only check dependence between stores and loads with regard
|
|
// to the loop nest it is created with.
|
|
//
|
|
// The analysis can output debugging information to a stream. The output
|
|
// describes the control flow of the analysis and what information it can deduce
|
|
// at each step.
|
|
// SetDebugStream and ClearDebugStream are provided for this functionality.
|
|
//
|
|
// The dependency algorithm is based on the 1990 Paper
|
|
// Practical Dependence Testing
|
|
// Gina Goff, Ken Kennedy, Chau-Wen Tseng
|
|
//
|
|
// The algorithm first identifies subscript pairs between the load and store.
|
|
// Each pair is tested until all have been tested or independence is found.
|
|
// The number of induction variables in a pair determines which test to perform
|
|
// on it;
|
|
// Zero Index Variable (ZIV) is used when no induction variables are present
|
|
// in the pair.
|
|
// Single Index Variable (SIV) is used when only one induction variable is
|
|
// present, but may occur multiple times in the pair.
|
|
// Multiple Index Variable (MIV) is used when more than one induction variable
|
|
// is present in the pair.
|
|
class LoopDependenceAnalysis {
|
|
public:
|
|
LoopDependenceAnalysis(ir::IRContext* context,
|
|
std::vector<const ir::Loop*> loops)
|
|
: context_(context),
|
|
loops_(loops),
|
|
scalar_evolution_(context),
|
|
debug_stream_(nullptr),
|
|
constraints_{} {}
|
|
|
|
// Finds the dependence between |source| and |destination|.
|
|
// |source| should be an OpLoad.
|
|
// |destination| should be an OpStore.
|
|
// Any direction and distance information found will be stored in
|
|
// |distance_vector|.
|
|
// Returns true if independence is found, false otherwise.
|
|
bool GetDependence(const ir::Instruction* source,
|
|
const ir::Instruction* destination,
|
|
DistanceVector* distance_vector);
|
|
|
|
// Returns true if |subscript_pair| represents a Zero Index Variable pair
|
|
// (ZIV)
|
|
bool IsZIV(const std::pair<SENode*, SENode*>& subscript_pair);
|
|
|
|
// Returns true if |subscript_pair| represents a Single Index Variable
|
|
// (SIV) pair
|
|
bool IsSIV(const std::pair<SENode*, SENode*>& subscript_pair);
|
|
|
|
// Returns true if |subscript_pair| represents a Multiple Index Variable
|
|
// (MIV) pair
|
|
bool IsMIV(const std::pair<SENode*, SENode*>& subscript_pair);
|
|
|
|
// Finds the lower bound of |loop| as an SENode* and returns the result.
|
|
// The lower bound is the starting value of the loops induction variable
|
|
SENode* GetLowerBound(const ir::Loop* loop);
|
|
|
|
// Finds the upper bound of |loop| as an SENode* and returns the result.
|
|
// The upper bound is the last value before the loop exit condition is met.
|
|
SENode* GetUpperBound(const ir::Loop* loop);
|
|
|
|
// Returns true if |value| is between |bound_one| and |bound_two| (inclusive).
|
|
bool IsWithinBounds(int64_t value, int64_t bound_one, int64_t bound_two);
|
|
|
|
// Finds the bounds of |loop| as upper_bound - lower_bound and returns the
|
|
// resulting SENode.
|
|
// If the operations can not be completed a nullptr is returned.
|
|
SENode* GetTripCount(const ir::Loop* loop);
|
|
|
|
// Returns the SENode* produced by building an SENode from the result of
|
|
// calling GetInductionInitValue on |loop|.
|
|
// If the operation can not be completed a nullptr is returned.
|
|
SENode* GetFirstTripInductionNode(const ir::Loop* loop);
|
|
|
|
// Returns the SENode* produced by building an SENode from the result of
|
|
// GetFirstTripInductionNode + (GetTripCount - 1) * induction_coefficient.
|
|
// If the operation can not be completed a nullptr is returned.
|
|
SENode* GetFinalTripInductionNode(const ir::Loop* loop,
|
|
SENode* induction_coefficient);
|
|
|
|
// Returns all the distinct loops that appear in |nodes|.
|
|
std::set<const ir::Loop*> CollectLoops(
|
|
const std::vector<SERecurrentNode*>& nodes);
|
|
|
|
// Returns all the distinct loops that appear in |source| and |destination|.
|
|
std::set<const ir::Loop*> CollectLoops(SENode* source, SENode* destination);
|
|
|
|
// Returns true if |distance| is provably outside the loop bounds.
|
|
// |coefficient| must be an SENode representing the coefficient of the
|
|
// induction variable of |loop|.
|
|
// This method is able to handle some symbolic cases which IsWithinBounds
|
|
// can't handle.
|
|
bool IsProvablyOutsideOfLoopBounds(const ir::Loop* loop, SENode* distance,
|
|
SENode* coefficient);
|
|
|
|
// Sets the ostream for debug information for the analysis.
|
|
void SetDebugStream(std::ostream& debug_stream) {
|
|
debug_stream_ = &debug_stream;
|
|
}
|
|
|
|
// Clears the stored ostream to stop debug information printing.
|
|
void ClearDebugStream() { debug_stream_ = nullptr; }
|
|
|
|
// Returns the ScalarEvolutionAnalysis used by this analysis.
|
|
ScalarEvolutionAnalysis* GetScalarEvolution() { return &scalar_evolution_; }
|
|
|
|
// Creates a new constraint of type |T| and returns the pointer to it.
|
|
template <typename T, typename... Args>
|
|
Constraint* make_constraint(Args&&... args) {
|
|
constraints_.push_back(
|
|
std::unique_ptr<Constraint>(new T(std::forward<Args>(args)...)));
|
|
|
|
return constraints_.back().get();
|
|
}
|
|
|
|
// Subscript partitioning as described in Figure 1 of 'Practical Dependence
|
|
// Testing' by Gina Goff, Ken Kennedy, and Chau-Wen Tseng from PLDI '91.
|
|
// Partitions the subscripts into independent subscripts and minimally coupled
|
|
// sets of subscripts.
|
|
// Returns the partitioning of subscript pairs. Sets of size 1 indicates an
|
|
// independent subscript-pair and others indicate coupled sets.
|
|
using PartitionedSubscripts =
|
|
std::vector<std::set<std::pair<ir::Instruction*, ir::Instruction*>>>;
|
|
PartitionedSubscripts PartitionSubscripts(
|
|
const std::vector<ir::Instruction*>& source_subscripts,
|
|
const std::vector<ir::Instruction*>& destination_subscripts);
|
|
|
|
// Returns the ir::Loop* matching the loop for |subscript_pair|.
|
|
// |subscript_pair| must be an SIV pair.
|
|
const ir::Loop* GetLoopForSubscriptPair(
|
|
const std::pair<SENode*, SENode*>& subscript_pair);
|
|
|
|
// Returns the DistanceEntry matching the loop for |subscript_pair|.
|
|
// |subscript_pair| must be an SIV pair.
|
|
DistanceEntry* GetDistanceEntryForSubscriptPair(
|
|
const std::pair<SENode*, SENode*>& subscript_pair,
|
|
DistanceVector* distance_vector);
|
|
|
|
// Returns the DistanceEntry matching |loop|.
|
|
DistanceEntry* GetDistanceEntryForLoop(const ir::Loop* loop,
|
|
DistanceVector* distance_vector);
|
|
|
|
// Returns a vector of Instruction* which form the subscripts of the array
|
|
// access defined by the access chain |instruction|.
|
|
std::vector<ir::Instruction*> GetSubscripts(
|
|
const ir::Instruction* instruction);
|
|
|
|
// Delta test as described in Figure 3 of 'Practical Dependence
|
|
// Testing' by Gina Goff, Ken Kennedy, and Chau-Wen Tseng from PLDI '91.
|
|
bool DeltaTest(
|
|
const std::vector<std::pair<SENode*, SENode*>>& coupled_subscripts,
|
|
DistanceVector* dv_entry);
|
|
|
|
// Constraint propagation as described in Figure 5 of 'Practical Dependence
|
|
// Testing' by Gina Goff, Ken Kennedy, and Chau-Wen Tseng from PLDI '91.
|
|
std::pair<SENode*, SENode*> PropagateConstraints(
|
|
const std::pair<SENode*, SENode*>& subscript_pair,
|
|
const std::vector<Constraint*>& constraints);
|
|
|
|
// Constraint intersection as described in Figure 4 of 'Practical Dependence
|
|
// Testing' by Gina Goff, Ken Kennedy, and Chau-Wen Tseng from PLDI '91.
|
|
Constraint* IntersectConstraints(Constraint* constraint_0,
|
|
Constraint* constraint_1,
|
|
const SENode* lower_bound,
|
|
const SENode* upper_bound);
|
|
|
|
// Returns true if each loop in |loops| is in a form supported by this
|
|
// analysis.
|
|
// A loop is supported if it has a single induction variable and that
|
|
// induction variable has a step of +1 or -1 per loop iteration.
|
|
bool CheckSupportedLoops(std::vector<const ir::Loop*> loops);
|
|
|
|
// Returns true if |loop| is in a form supported by this analysis.
|
|
// A loop is supported if it has a single induction variable and that
|
|
// induction variable has a step of +1 or -1 per loop iteration.
|
|
bool IsSupportedLoop(const ir::Loop* loop);
|
|
|
|
private:
|
|
ir::IRContext* context_;
|
|
|
|
// The loop nest we are analysing the dependence of.
|
|
std::vector<const ir::Loop*> loops_;
|
|
|
|
// The ScalarEvolutionAnalysis used by this analysis to store and perform much
|
|
// of its logic.
|
|
ScalarEvolutionAnalysis scalar_evolution_;
|
|
|
|
// The ostream debug information for the analysis to print to.
|
|
std::ostream* debug_stream_;
|
|
|
|
// Stores all the constraints created by the analysis.
|
|
std::list<std::unique_ptr<Constraint>> constraints_;
|
|
|
|
// Returns true if independence can be proven and false if it can't be proven.
|
|
bool ZIVTest(const std::pair<SENode*, SENode*>& subscript_pair);
|
|
|
|
// Analyzes the subscript pair to find an applicable SIV test.
|
|
// Returns true if independence can be proven and false if it can't be proven.
|
|
bool SIVTest(const std::pair<SENode*, SENode*>& subscript_pair,
|
|
DistanceVector* distance_vector);
|
|
|
|
// Takes the form a*i + c1, a*i + c2
|
|
// When c1 and c2 are loop invariant and a is constant
|
|
// distance = (c1 - c2)/a
|
|
// < if distance > 0
|
|
// direction = = if distance = 0
|
|
// > if distance < 0
|
|
// Returns true if independence is proven and false if it can't be proven.
|
|
bool StrongSIVTest(SENode* source, SENode* destination, SENode* coeff,
|
|
DistanceEntry* distance_entry);
|
|
|
|
// Takes for form a*i + c1, a*i + c2
|
|
// where c1 and c2 are loop invariant and a is constant.
|
|
// c1 and/or c2 contain one or more SEValueUnknown nodes.
|
|
bool SymbolicStrongSIVTest(SENode* source, SENode* destination,
|
|
SENode* coefficient,
|
|
DistanceEntry* distance_entry);
|
|
|
|
// Takes the form a1*i + c1, a2*i + c2
|
|
// where a1 = 0
|
|
// distance = (c1 - c2) / a2
|
|
// Returns true if independence is proven and false if it can't be proven.
|
|
bool WeakZeroSourceSIVTest(SENode* source, SERecurrentNode* destination,
|
|
SENode* coefficient,
|
|
DistanceEntry* distance_entry);
|
|
|
|
// Takes the form a1*i + c1, a2*i + c2
|
|
// where a2 = 0
|
|
// distance = (c2 - c1) / a1
|
|
// Returns true if independence is proven and false if it can't be proven.
|
|
bool WeakZeroDestinationSIVTest(SERecurrentNode* source, SENode* destination,
|
|
SENode* coefficient,
|
|
DistanceEntry* distance_entry);
|
|
|
|
// Takes the form a1*i + c1, a2*i + c2
|
|
// where a1 = -a2
|
|
// distance = (c2 - c1) / 2*a1
|
|
// Returns true if independence is proven and false if it can't be proven.
|
|
bool WeakCrossingSIVTest(SENode* source, SENode* destination,
|
|
SENode* coefficient, DistanceEntry* distance_entry);
|
|
|
|
// Uses the def_use_mgr to get the instruction referenced by
|
|
// SingleWordInOperand(|id|) when called on |instruction|.
|
|
ir::Instruction* GetOperandDefinition(const ir::Instruction* instruction,
|
|
int id);
|
|
|
|
// Perform the GCD test if both, the source and the destination nodes, are in
|
|
// the form a0*i0 + a1*i1 + ... an*in + c.
|
|
bool GCDMIVTest(const std::pair<SENode*, SENode*>& subscript_pair);
|
|
|
|
// Finds the number of induction variables in |node|.
|
|
// Returns -1 on failure.
|
|
int64_t CountInductionVariables(SENode* node);
|
|
|
|
// Finds the number of induction variables shared between |source| and
|
|
// |destination|.
|
|
// Returns -1 on failure.
|
|
int64_t CountInductionVariables(SENode* source, SENode* destination);
|
|
|
|
// Takes the offset from the induction variable and subtracts the lower bound
|
|
// from it to get the constant term added to the induction.
|
|
// Returns the resuting constant term, or nullptr if it could not be produced.
|
|
SENode* GetConstantTerm(const ir::Loop* loop, SERecurrentNode* induction);
|
|
|
|
// Marks all the distance entries in |distance_vector| that were relate to
|
|
// loops in |loops_| but were not used in any subscripts as irrelevant to the
|
|
// to the dependence test.
|
|
void MarkUnsusedDistanceEntriesAsIrrelevant(
|
|
const ir::Instruction* source, const ir::Instruction* destination,
|
|
DistanceVector* distance_vector);
|
|
|
|
// Converts |value| to a std::string and returns the result.
|
|
// This is required because Android does not compile std::to_string.
|
|
template <typename valueT>
|
|
std::string ToString(valueT value) {
|
|
std::ostringstream string_stream;
|
|
string_stream << value;
|
|
return string_stream.str();
|
|
}
|
|
|
|
// Prints |debug_msg| and "\n" to the ostream pointed to by |debug_stream_|.
|
|
// Won't print anything if |debug_stream_| is nullptr.
|
|
void PrintDebug(std::string debug_msg);
|
|
};
|
|
|
|
} // namespace opt
|
|
} // namespace spvtools
|
|
|
|
#endif // SOURCE_OPT_LOOP_DEPENDENCE_H__
|