[DeLICM] Add Knowledge class. NFC.

The Knowledge class remembers the state of data at any timepoint of a SCoP's
execution. Currently, it tracks whether an array element is unused or is
occupied by some value, and the writes to it. A future addition will be to also
remember which value it contains.

Objects are used to determine whether two Knowledge contain conflicting
information, i.e. two states cannot be true a the same time.

This commit was extracted from the DeLICM algorithm at
https://reviews.llvm.org/D24716.

llvm-svn: 295197
This commit is contained in:
Michael Kruse 2017-02-15 16:59:10 +00:00
parent fbe32f59c2
commit e23e94a08d
3 changed files with 378 additions and 1 deletions

View File

@ -18,6 +18,8 @@
#ifndef POLLY_DELICM_H #ifndef POLLY_DELICM_H
#define POLLY_DELICM_H #define POLLY_DELICM_H
#include "polly/Support/GICHelper.h"
namespace llvm { namespace llvm {
class PassRegistry; class PassRegistry;
class Pass; class Pass;
@ -26,6 +28,17 @@ class Pass;
namespace polly { namespace polly {
/// Create a new DeLICM pass instance. /// Create a new DeLICM pass instance.
llvm::Pass *createDeLICMPass(); llvm::Pass *createDeLICMPass();
/// Determine whether two lifetimes are conflicting.
///
/// Used by unittesting.
bool isConflicting(IslPtr<isl_union_set> ExistingOccupied,
IslPtr<isl_union_set> ExistingUnused,
IslPtr<isl_union_set> ExistingWrites,
IslPtr<isl_union_set> ProposedOccupied,
IslPtr<isl_union_set> ProposedUnused,
IslPtr<isl_union_set> ProposedWrites,
llvm::raw_ostream *OS = nullptr, unsigned Indent = 0);
} // namespace polly } // namespace polly
namespace llvm { namespace llvm {

View File

@ -13,11 +13,106 @@
// Namely, remove register/scalar dependencies by mapping them back to array // Namely, remove register/scalar dependencies by mapping them back to array
// elements. // elements.
// //
// The algorithms here work on the scatter space - the image space of the
// schedule returned by Scop::getSchedule(). We call an element in that space a
// "timepoint". Timepoints are lexicographically ordered such that we can
// defined ranges in the scatter space. We use two flavors of such ranges:
// Timepoint sets and zones. A timepoint set is simply a subset of the scatter
// space and is directly stored as isl_set.
//
// Zones are used to describe the space between timepoints as open sets, i.e.
// they do not contain the extrema. Using isl rational sets to express these
// would be overkill. We also cannot store them as the integer timepoints they
// contain; the (nonempty) zone between 1 and 2 would be empty and
// indistinguishable from e.g. the zone between 3 and 4. Also, we cannot store
// the integer set including the extrema; the set ]1,2[ + ]3,4[ could be
// coalesced to ]1,3[, although we defined the range [2,3] to be not in the set.
// Instead, we store the "half-open" integer extrema, including the lower bound,
// but excluding the upper bound. Examples:
//
// * The set { [i] : 1 <= i <= 3 } represents the zone ]0,3[ (which contains the
// integer points 1 and 2, but not 0 or 3)
//
// * { [1] } represents the zone ]0,1[
//
// * { [i] : i = 1 or i = 3 } represents the zone ]0,1[ + ]2,3[
//
// Therefore, an integer i in the set represents the zone ]i-1,i[, i.e. strictly
// speaking the integer points never belong to the zone. However, depending an
// the interpretation, one might want to include them. Part of the
// interpretation may not be known when the zone is constructed.
//
// Reads are assumed to always take place before writes, hence we can think of
// reads taking place at the beginning of a timepoint and writes at the end.
//
// Let's assume that the zone represents the lifetime of a variable. That is,
// the zone begins with a write that defines the value during its lifetime and
// ends with the last read of that value. In the following we consider whether a
// read/write at the beginning/ending of the lifetime zone should be within the
// zone or outside of it.
//
// * A read at the timepoint that starts the live-range loads the previous
// value. Hence, exclude the timepoint starting the zone.
//
// * A write at the timepoint that starts the live-range is not defined whether
// it occurs before or after the write that starts the lifetime. We do not
// allow this situation to occur. Hence, we include the timepoint starting the
// zone to determine whether they are conflicting.
//
// * A read at the timepoint that ends the live-range reads the same variable.
// We include the timepoint at the end of the zone to include that read into
// the live-range. Doing otherwise would mean that the two reads access
// different values, which would mean that the value they read are both alive
// at the same time but occupy the same variable.
//
// * A write at the timepoint that ends the live-range starts a new live-range.
// It must not be included in the live-range of the previous definition.
//
// All combinations of reads and writes at the endpoints are possible, but most
// of the time only the write->read (for instance, a live-range from definition
// to last use) and read->write (for instance, an unused range from last use to
// overwrite) and combinations are interesting (half-open ranges). write->write
// zones might be useful as well in some context to represent
// output-dependencies.
//
// @see convertZoneToTimepoints
//
//
// The code makes use of maps and sets in many different spaces. To not loose
// track in which space a set or map is expected to be in, variables holding an
// isl reference are usually annotated in the comments. They roughly follow isl
// syntax for spaces, but only the tuples, not the dimensions. The tuples have a
// meaning as follows:
//
// * Space[] - An unspecified tuple. Used for function parameters such that the
// function caller can use it for anything they like.
//
// * Domain[] - A statement instance as returned by ScopStmt::getDomain()
// isl_id_get_name: Stmt_<NameOfBasicBlock>
// isl_id_get_user: Pointer to ScopStmt
//
// * Element[] - An array element as in the range part of
// MemoryAccess::getAccessRelation()
// isl_id_get_name: MemRef_<NameOfArrayVariable>
// isl_id_get_user: Pointer to ScopArrayInfo
//
// * Scatter[] - Scatter space or space of timepoints
// Has no tuple id
//
// * Zone[] - Range between timepoints as described above
// Has no tuple id
//
// An annotation "{ Domain[] -> Scatter[] }" therefore means: A map from a
// statement instance to a timepoint, aka a schedule. There is only one scatter
// space, but most of the time multiple statements are processed in one set.
// This is why most of the time isl_union_map has to be used.
//
//===----------------------------------------------------------------------===// //===----------------------------------------------------------------------===//
#include "polly/DeLICM.h" #include "polly/DeLICM.h"
#include "polly/ScopInfo.h" #include "polly/ScopInfo.h"
#include "polly/ScopPass.h" #include "polly/ScopPass.h"
#include "polly/Support/ISLTools.h"
#define DEBUG_TYPE "polly-delicm" #define DEBUG_TYPE "polly-delicm"
using namespace polly; using namespace polly;
@ -25,6 +120,259 @@ using namespace llvm;
namespace { namespace {
/// Represent the knowledge of the contents of any array elements in any zone or
/// the knowledge we would add when mapping a scalar to an array element.
///
/// Every array element at every zone unit has one of two states:
///
/// - Unused: Not occupied by any value so a transformation can change it to
/// other values.
///
/// - Occupied: The element contains a value that is still needed.
///
/// The union of Unused and Unknown zones forms the universe, the set of all
/// elements at every timepoint. The universe can easily be derived from the
/// array elements that are accessed someway. Arrays that are never accessed
/// also never play a role in any computation and can hence be ignored. With a
/// given universe, only one of the sets needs to stored implicitly. Computing
/// the complement is also an expensive operation, hence this class has been
/// designed that only one of sets is needed while the other is assumed to be
/// implicit. It can still be given, but is mostly ignored.
///
/// There are two use cases for the Knowledge class:
///
/// 1) To represent the knowledge of the current state of ScopInfo. The unused
/// state means that an element is currently unused: there is no read of it
/// before the next overwrite. Also called 'Existing'.
///
/// 2) To represent the requirements for mapping a scalar to array elements. The
/// unused state means that there is no change/requirement. Also called
/// 'Proposed'.
///
/// In addition to these states at unit zones, Knowledge needs to know when
/// values are written. This is because written values may have no lifetime (one
/// reason is that the value is never read). Such writes would therefore never
/// conflict, but overwrite values that might still be required. Another source
/// of problems are multiple writes to the same element at the same timepoint,
/// because their order is undefined.
class Knowledge {
private:
/// { [Element[] -> Zone[]] }
/// Set of array elements and when they are alive.
/// Can contain a nullptr; in this case the set is implicitly defined as the
/// complement of #Unused.
///
/// The set of alive array elements is represented as zone, as the set of live
/// values can differ depending on how the elements are interpreted.
/// Assuming a value X is written at timestep [0] and read at timestep [1]
/// without being used at any later point, then the value is alive in the
/// interval ]0,1[. This interval cannot be represented by an integer set, as
/// it does not contain any integer point. Zones allow us to represent this
/// interval and can be converted to sets of timepoints when needed (e.g., in
/// isConflicting when comparing to the write sets).
/// @see convertZoneToTimepoints and this file's comment for more details.
IslPtr<isl_union_set> Occupied;
/// { [Element[] -> Zone[]] }
/// Set of array elements when they are not alive, i.e. their memory can be
/// used for other purposed. Can contain a nullptr; in this case the set is
/// implicitly defined as the complement of #Occupied.
IslPtr<isl_union_set> Unused;
/// { [Element[] -> Scatter[]] }
/// The write actions currently in the scop or that would be added when
/// mapping a scalar.
IslPtr<isl_union_set> Written;
/// Check whether this Knowledge object is well-formed.
void checkConsistency() const {
#ifndef NDEBUG
// Default-initialized object
if (!Occupied && !Unused && !Written)
return;
assert(Occupied || Unused);
assert(Written);
// If not all fields are defined, we cannot derived the universe.
if (!Occupied || !Unused)
return;
assert(isl_union_set_is_disjoint(Occupied.keep(), Unused.keep()) ==
isl_bool_true);
auto Universe = give(isl_union_set_union(Occupied.copy(), Unused.copy()));
assert(isl_union_set_is_subset(Written.keep(), Universe.keep()) ==
isl_bool_true);
#endif
}
public:
/// Initialize a nullptr-Knowledge. This is only provided for convenience; do
/// not use such an object.
Knowledge() {}
/// Create a new object with the given members.
Knowledge(IslPtr<isl_union_set> Occupied, IslPtr<isl_union_set> Unused,
IslPtr<isl_union_set> Written)
: Occupied(std::move(Occupied)), Unused(std::move(Unused)),
Written(std::move(Written)) {
checkConsistency();
}
/// Alternative constructor taking isl_sets instead isl_union_sets.
Knowledge(IslPtr<isl_set> Occupied, IslPtr<isl_set> Unused,
IslPtr<isl_set> Written)
: Knowledge(give(isl_union_set_from_set(Occupied.take())),
give(isl_union_set_from_set(Unused.take())),
give(isl_union_set_from_set(Written.take()))) {}
/// Return whether this object was not default-constructed.
bool isUsable() const { return (Occupied || Unused) && Written; }
/// Print the content of this object to @p OS.
void print(llvm::raw_ostream &OS, unsigned Indent = 0) const {
if (isUsable()) {
if (Occupied)
OS.indent(Indent) << "Occupied: " << Occupied << "\n";
else
OS.indent(Indent) << "Occupied: <Everything else not in Unused>\n";
if (Unused)
OS.indent(Indent) << "Unused: " << Unused << "\n";
else
OS.indent(Indent) << "Unused: <Everything else not in Occupied>\n";
OS.indent(Indent) << "Written : " << Written << '\n';
} else {
OS.indent(Indent) << "Invalid knowledge\n";
}
}
/// Combine two knowledges, this and @p That.
void learnFrom(Knowledge That) {
assert(!isConflicting(*this, That));
assert(Unused && That.Occupied);
assert(
!That.Unused &&
"This function is only prepared to learn occupied elements from That");
assert(!Occupied && "This function does not implement "
"`this->Occupied = "
"give(isl_union_set_union(this->Occupied.take(), "
"That.Occupied.copy()));`");
Unused = give(isl_union_set_subtract(Unused.take(), That.Occupied.copy()));
Written = give(isl_union_set_union(Written.take(), That.Written.take()));
checkConsistency();
}
/// Determine whether two Knowledges conflict with each other.
///
/// In theory @p Existing and @p Proposed are symmetric, but the
/// implementation is constrained by the implicit interpretation. That is, @p
/// Existing must have #Unused defined (use case 1) and @p Proposed must have
/// #Occupied defined (use case 1).
///
/// A conflict is defined as non-preserved semantics when they are merged. For
/// instance, when for the same array and zone they assume different
/// llvm::Values.
///
/// @param Existing One of the knowledges with #Unused defined.
/// @param Proposed One of the knowledges with #Occupied defined.
/// @param OS Dump the conflict reason to this output stream; use
/// nullptr to not output anything.
/// @param Indent Indention for the conflict reason.
///
/// @return True, iff the two knowledges are conflicting.
static bool isConflicting(const Knowledge &Existing,
const Knowledge &Proposed,
llvm::raw_ostream *OS = nullptr,
unsigned Indent = 0) {
assert(Existing.Unused);
assert(Proposed.Occupied);
#ifndef NDEBUG
if (Existing.Occupied && Proposed.Unused) {
auto ExistingUniverse = give(isl_union_set_union(Existing.Occupied.copy(),
Existing.Unused.copy()));
auto ProposedUniverse = give(isl_union_set_union(Proposed.Occupied.copy(),
Proposed.Unused.copy()));
assert(isl_union_set_is_equal(ExistingUniverse.keep(),
ProposedUniverse.keep()) == isl_bool_true &&
"Both inputs' Knowledges must be over the same universe");
}
#endif
// Are the new lifetimes required for Proposed unused in Existing?
if (isl_union_set_is_subset(Proposed.Occupied.keep(),
Existing.Unused.keep()) != isl_bool_true) {
if (OS) {
auto ConflictingLifetimes = give(isl_union_set_subtract(
Proposed.Occupied.copy(), Existing.Unused.copy()));
OS->indent(Indent) << "Proposed lifetimes are not unused in existing\n";
OS->indent(Indent) << "Conflicting lifetimes: " << ConflictingLifetimes
<< "\n";
}
return true;
}
// Do the writes in Existing only overwrite unused values in Proposed?
// We convert here the set of lifetimes to actual timepoints. A lifetime is
// in conflict with a set of write timepoints, if either a live timepoint is
// clearly within the lifetime or if a write happens at the beginning of the
// lifetime (where it would conflict with the value that actually writes the
// value alive). There is no conflict at the end of a lifetime, as the alive
// value will always be read, before it is overwritten again. The last
// property holds in Polly for all scalar values and we expect all users of
// Knowledge to check this property also for accesses to MemoryKind::Array.
auto ProposedFixedDefs =
convertZoneToTimepoints(Proposed.Occupied, true, false);
if (isl_union_set_is_disjoint(Existing.Written.keep(),
ProposedFixedDefs.keep()) != isl_bool_true) {
if (OS) {
auto ConflictingWrites = give(isl_union_set_intersect(
Existing.Written.copy(), ProposedFixedDefs.copy()));
OS->indent(Indent) << "Proposed writes into range used by existing\n";
OS->indent(Indent) << "Conflicting writes: " << ConflictingWrites
<< "\n";
}
return true;
}
// Do the new writes in Proposed only overwrite unused values in Existing?
auto ExistingAvailableDefs =
convertZoneToTimepoints(Existing.Unused, true, false);
if (isl_union_set_is_subset(Proposed.Written.keep(),
ExistingAvailableDefs.keep()) !=
isl_bool_true) {
if (OS) {
auto ConflictingWrites = give(isl_union_set_subtract(
Proposed.Written.copy(), ExistingAvailableDefs.copy()));
OS->indent(Indent)
<< "Proposed a lifetime where there is an Existing write into it\n";
OS->indent(Indent) << "Conflicting writes: " << ConflictingWrites
<< "\n";
}
return true;
}
// Does Proposed write at the same time as Existing already does (order of
// writes is undefined)?
if (isl_union_set_is_disjoint(Existing.Written.keep(),
Proposed.Written.keep()) != isl_bool_true) {
if (OS) {
auto ConflictingWrites = give(isl_union_set_intersect(
Existing.Written.copy(), Proposed.Written.copy()));
OS->indent(Indent) << "Proposed writes at the same time as an already "
"Existing write\n";
OS->indent(Indent) << "Conflicting writes: " << ConflictingWrites
<< "\n";
}
return true;
}
return false;
}
};
class DeLICM : public ScopPass { class DeLICM : public ScopPass {
private: private:
DeLICM(const DeLICM &) = delete; DeLICM(const DeLICM &) = delete;
@ -68,3 +416,18 @@ INITIALIZE_PASS_BEGIN(DeLICM, "polly-delicm", "Polly - DeLICM/DePRE", false,
INITIALIZE_PASS_DEPENDENCY(ScopInfoWrapperPass) INITIALIZE_PASS_DEPENDENCY(ScopInfoWrapperPass)
INITIALIZE_PASS_END(DeLICM, "polly-delicm", "Polly - DeLICM/DePRE", false, INITIALIZE_PASS_END(DeLICM, "polly-delicm", "Polly - DeLICM/DePRE", false,
false) false)
bool polly::isConflicting(IslPtr<isl_union_set> ExistingOccupied,
IslPtr<isl_union_set> ExistingUnused,
IslPtr<isl_union_set> ExistingWrites,
IslPtr<isl_union_set> ProposedOccupied,
IslPtr<isl_union_set> ProposedUnused,
IslPtr<isl_union_set> ProposedWrites,
llvm::raw_ostream *OS, unsigned Indent) {
Knowledge Existing(std::move(ExistingOccupied), std::move(ExistingUnused),
std::move(ExistingWrites));
Knowledge Proposed(std::move(ProposedOccupied), std::move(ProposedUnused),
std::move(ProposedWrites));
return Knowledge::isConflicting(Existing, Proposed, OS, Indent);
}

View File

@ -16,8 +16,9 @@ function(add_polly_unittest test_name)
set_property(TARGET ${test_name} PROPERTY FOLDER "Polly") set_property(TARGET ${test_name} PROPERTY FOLDER "Polly")
endif() endif()
target_link_libraries(${test_name} Polly LLVMCore LLVMSupport LLVMDemangle) target_link_libraries(${test_name} Polly LLVMCore LLVMSupport LLVMDemangle LLVMipo)
endfunction() endfunction()
add_subdirectory(Isl) add_subdirectory(Isl)
add_subdirectory(Flatten) add_subdirectory(Flatten)
add_subdirectory(DeLICM)