Files
revng-revng/lib/DataLayoutAnalysis/Middleend/CompactCompatibleArrays.cpp
T
Pietro Fezzardi 7ea3b507df DLA: CompactCompatibleArrays on non-strided
This commit extends the CompactCompatibleArrays DLAStep to also consider
non-strided accesses.
Strided accesses are still always considered first, and they are still
considered the only real source of information on arrays, but after
having tried to compact all the compatible arrays,
CompactCompatibleArrays now also considers non-strided instance edges to
see if they can be compacted with the rest of the inferred array.
This has shown to handle gracefully a number of real-world examples and
reduce unions.
2023-12-05 16:59:08 +01:00

506 lines
20 KiB
C++

//
// Copyright (c) rev.ng Labs Srl. See LICENSE.md for details.
//
#include <compare>
#include <functional>
#include <optional>
#include "llvm/ADT/GraphTraits.h"
#include "llvm/ADT/PostOrderIterator.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/ADT/SetVector.h"
#include "llvm/ADT/SmallSet.h"
#include "llvm/ADT/SmallVector.h"
#include "DLAStep.h"
static uint64_t
getTripCount(uint64_t StartOffset, uint64_t EndOffset, uint64_t Stride) {
revng_assert(EndOffset > StartOffset);
auto DivRem = std::lldiv(EndOffset - StartOffset, Stride);
return DivRem.quot + (DivRem.rem ? 1ULL : 0ULL);
}
namespace dla {
using NeighborIterator = LayoutTypeSystem::NeighborIterator;
// Helper ordering for NeighborIterators. We need it here because we need to use
// such iterators and keys in associative containers, and we want neighbors with
// lower offset to come first.
// Notice that this might have undefined behavior if dereferencing either LHS
// or RHS is undefined behavior itself.
// The bottom line is that we should never insert invalid iterators into
// associative containers.
static std::weak_ordering operator<=>(const NeighborIterator &LHS,
const NeighborIterator &RHS) {
const auto &[LHSSucc, LHSTag] = *LHS;
const auto &[RHSSucc, RHSTag] = *RHS;
if (auto Cmp = LHSTag <=> RHSTag; Cmp != 0)
return Cmp;
return LHSSucc <=> RHSSucc;
}
struct InstanceEdge {
OffsetExpression OE;
LayoutTypeSystemNode *Target;
InstanceEdge() = default;
template<typename OffsetExpressionT>
requires std::is_same_v<std::remove_cvref_t<OffsetExpressionT>,
OffsetExpression>
InstanceEdge(OffsetExpressionT &&O, LayoutTypeSystemNode *N) :
OE(std::forward<OffsetExpressionT>(O)), Target(N) {}
template<typename OffsetExpressionT>
requires std::is_same_v<std::remove_cvref_t<OffsetExpressionT>,
OffsetExpression>
InstanceEdge(OffsetExpressionT &&O) :
InstanceEdge(std::forward<OffsetExpressionT>(O), nullptr) {}
InstanceEdge(const LayoutTypeSystemNode::Link &L) :
InstanceEdge(L.second->getOffsetExpr(), L.first) {}
InstanceEdge(const InstanceEdge &) = default;
InstanceEdge &operator=(const InstanceEdge &) = default;
InstanceEdge(InstanceEdge &&) = default;
InstanceEdge &operator=(InstanceEdge &&) = default;
// Ordering and comparison
std::strong_ordering operator<=>(const InstanceEdge &) const = default;
bool operator==(const InstanceEdge &) const = default;
};
/// Simple struct representing all the information we need to track on an array
/// to be created compacting many strided instance edges.
struct CompactedArrayInfo {
// The start offset of the array inside the struct that owns it.
uint64_t StartOffset;
// The end offset of the array inside the struct that owns it.
uint64_t EndOffset;
// The stride of the array.
uint64_t Stride;
// 1 + the highest offset inside the array element for which DLA can guarantee
// that it can see a memory access.
// Should always be > 0 and <= Stride.
uint64_t AccessedElementSize;
// The number of bytes available outside the array, in the owning struct,
// before the array itself.
// It is an upper bound on how much it is possible to decrease the StartOffset
// of the array.
// It's always required to be at most (Stride - AccessedElementSize),
// otherwise it would be possible to decrease the StartOffset to "shift" the
// array in a way that would leave part of the AccessedElementSize out.
uint64_t Slack;
bool operator==(const CompactedArrayInfo &) const = default;
std::strong_ordering operator<=>(const CompactedArrayInfo &) const = default;
};
CompactedArrayInfo makeCompactedArrayInfo(const auto &ArrayEdge,
uint64_t OuterStride = 0ULL) {
revng_assert(isInstanceEdge(ArrayEdge));
const auto &[Child, OE] = ArrayEdge;
const auto &[Offset, Strides, TripCounts] = OE->getOffsetExpr();
revng_assert(Strides.size() <= 1ULL);
revng_assert(Strides.size() == 1ULL xor OuterStride != 0ULL);
revng_assert(TripCounts.size() <= 1ULL);
revng_assert(TripCounts.size() == 1ULL xor OuterStride != 0ULL);
uint64_t ChildSize = Child->Size;
revng_assert(ChildSize > 0ULL);
uint64_t Stride = OuterStride ? std::max(OuterStride, ChildSize) :
Strides.front();
revng_assert(Stride >= ChildSize);
uint64_t TripCount = OuterStride ? 1ULL : TripCounts.front().value_or(1ULL);
revng_assert(TripCount > 0ULL);
uint64_t EndOffset = Offset + (Stride * (TripCount - 1ULL)) + ChildSize;
uint64_t Slack = std::min(Offset, Stride - ChildSize);
return CompactedArrayInfo{
.StartOffset = Offset,
.EndOffset = EndOffset,
.Stride = Stride,
.AccessedElementSize = ChildSize,
.Slack = Slack,
};
}
/// Tries to adjust ToShift slightly moving it to lower StartOffset, so that
/// its element is aligned with AlignTo's element.
/// If this is successful, it then tries to compact the two resulting arrays, to
/// create a larger array that contains both the shifted ToShift, and AlignTo.
static std::optional<CompactedArrayInfo>
tryShiftLowerAndCompact(const CompactedArrayInfo &ToShift,
const CompactedArrayInfo &AlignTo) {
revng_assert(ToShift.Stride == AlignTo.Stride);
uint64_t Stride = ToShift.Stride;
// Compute the RequiredSlack, i.e. the number of bytes we have to shift
// ToShift towards a lower StartOffset, to align its element with ToAlign's
// element.
uint64_t RequiredSlack = 0ULL;
{
// Consider AlignTo's element aligned to 0, because that's the baseline we
// want to align ToShift to.
uint64_t ToShiftAlignment = ToShift.StartOffset % Stride;
uint64_t ToMatchAlignment = AlignTo.StartOffset % Stride;
if (ToShiftAlignment > ToMatchAlignment) {
// If ToShiftAlignment is larger than ToMatchAlignment, we just shift it
// down by ToMatchAlignment.
RequiredSlack = ToShiftAlignment - ToMatchAlignment;
} else if (ToShiftAlignment < ToMatchAlignment) {
// Otherwise, we have to add Stride first, otherwise we would underflow.
RequiredSlack = ToShiftAlignment + Stride - ToMatchAlignment;
}
}
revng_assert(RequiredSlack < Stride);
// So now the two elements of the arrays are aligned like this:
// AlignTo |<------------Stride------------>|
// ToShift |<--RequiredSlack-->|<------------Stride------------>|
// Now we'd like to try to shift ToShift to a lower starting offset, so that
// its starting point matches the starting point of AlignTo.
// We can only do that if ToShift has enough Slack.
if (ToShift.Slack < RequiredSlack)
return std::nullopt;
// Now we're shifting ToShift towards lower starting addresses, so we have to
// decrease the StartOffset accordingly.
// We can assume that ToShift.StartOffset is always larger or equal to
// RequiredSlack because of how ToShift was generated and because of the fact
// that StartOffset is part of the computation used to determine the value of
// ToShift.Slack.
// Also, AlignTo could start at a lower offset than ToShift, so we actually
// have to take the minimum.
revng_assert(ToShift.StartOffset >= RequiredSlack);
uint64_t NewStartOffset = std::min(ToShift.StartOffset - RequiredSlack,
AlignTo.StartOffset);
// We can obtain the NewSlack by subtracting the RequiredSlack from
// ToShift.Slack. This is not enough though, because AlignTo.Slack could be
// even smaller, so we have to take the minimum.
uint64_t NewSlack = std::min(ToShift.Slack - RequiredSlack, AlignTo.Slack);
// We can obtain the NewElement size by adding the RequiredSlack to
// ToShift.AccessedElementSize. Again, this is not enough, because
// AlignTo.AccessedElementSize could be even larger, so we have to take the
// maximum.
revng_assert(ToShift.AccessedElementSize <= Stride - RequiredSlack);
uint64_t NewElementSize = std::max(ToShift.AccessedElementSize
+ RequiredSlack,
AlignTo.AccessedElementSize);
// Compute the new TripCount.
// This a little bit tricky, because it might be tempting to just say that
// we can compute the new EndOffset as max(ToShift.EndOffset,
// AlignTo.EndOffset) and then compute the TripCount as
// getTripCount(NewStartOffset, EndOffset, Stride).
// This is wrong when the array with higher EndOffset also has a larger Slack
// (or a smaller AccessedElementSize, which in this case should be
// equivalent).
// The correct way to compute this is to compute the new TripCount as the max
// of the TripCounts to go from NewStartOffset to both ToShift.EndOffset and
// ToFit.EndOffset, that are never changed by previous computations.
// Then, with this new TripCount we can compute the new EndOffset considering
// the new AccessedElementSize and the Stride.
uint64_t TripCountA = getTripCount(NewStartOffset, ToShift.EndOffset, Stride);
uint64_t TripCountB = getTripCount(NewStartOffset, AlignTo.EndOffset, Stride);
uint64_t TripCount = std::max(TripCountA, TripCountB);
// The common EndOffset is the highest among the two.
uint64_t NewEndOffset = NewStartOffset + (Stride * (TripCount - 1ULL))
+ NewElementSize;
// If the new EndOffset is larger than the both the EndOffsets we're trying to
// compact, this compaction would introduce stuff to the right of what we've
// seen in the binary.
// This is something we're not allowed to do, because it might mess up the
// size of the containing struct.
if (NewEndOffset > std::max(ToShift.EndOffset, AlignTo.EndOffset))
return std::nullopt;
return CompactedArrayInfo{
.StartOffset = NewStartOffset,
.EndOffset = NewEndOffset,
.Stride = Stride,
.AccessedElementSize = NewElementSize,
.Slack = NewSlack,
};
}
static std::optional<CompactedArrayInfo> &
pickBest(std::optional<CompactedArrayInfo> &MaybeA,
std::optional<CompactedArrayInfo> &MaybeB) {
if (not MaybeA.has_value())
return MaybeB;
if (not MaybeB.has_value())
return MaybeA;
// Here both inputs have a value
CompactedArrayInfo &A = MaybeA.value();
CompactedArrayInfo &B = MaybeB.value();
// Should have the same Stride
revng_assert(A.Stride == B.Stride);
// Should be aligned
revng_assert((A.StartOffset % A.Stride) == (B.StartOffset % B.Stride));
// Should end in the same place.
// If they didn't, at least one of them should have increased the maximum
// EndOffset among the two of them, which should be filtered away.
revng_assert(A.EndOffset == B.EndOffset);
// If they don't start at the same StartOffset, the one with the highest
// StartOffset should be picked as best, because it generates less data at the
// beginning.
if (auto Cmp = A.StartOffset <=> B.StartOffset; Cmp != 0)
return (Cmp < 0) ? MaybeB : MaybeA;
// Because of how the two candidates are constructed we should have the
// guarantee that if the new start offsets are the same, then both Slack and
// AccessedElementSize are the same.
revng_assert(A == B);
return MaybeA;
}
using GT = llvm::GraphTraits<LayoutTypeSystemNode *>;
using llvm::SetVector;
using llvm::SmallSet;
using llvm::SmallVector;
using CompactedEdgeVector = SetVector<NeighborIterator,
SmallVector<NeighborIterator, 8>,
SmallSet<NeighborIterator, 8>>;
template<bool StridedEdges>
static CompactedArrayInfo
getEdgeToCompactWithCurrent(LayoutTypeSystemNode *Parent,
CompactedArrayInfo Current,
CompactedEdgeVector &CompactedWithCurrent) {
constexpr auto *EdgeShouldBeConsidered = StridedEdges ? isStridedInstance :
isNonStridedInstance;
const uint64_t OuterStride = StridedEdges ? 0ULL : Current.Stride;
auto SiblingEdgeIt = GT::child_edge_begin(Parent);
auto SiblingEdgeNext = SiblingEdgeIt;
auto SiblingEdgeEnd = GT::child_edge_end(Parent);
for (; SiblingEdgeIt != SiblingEdgeEnd; SiblingEdgeIt = SiblingEdgeNext) {
SiblingEdgeNext = std::next(SiblingEdgeIt);
// If we have already compacted that, skip it.
if (CompactedWithCurrent.count(SiblingEdgeIt) > 0)
continue;
// Ignore edges that shouldn't be considered.
const auto &ArraySiblingEdge = *SiblingEdgeIt;
if (not EdgeShouldBeConsidered(ArraySiblingEdge))
continue;
CompactedArrayInfo Sibling = makeCompactedArrayInfo(ArraySiblingEdge,
OuterStride);
// If the Sibling has a mismatching stride we don't compact it.
if (Sibling.Stride != Current.Stride)
continue;
// If the Sibling starts after the Current ends, they don't overlap
// and we don't compact them.
if (Sibling.StartOffset >= Current.EndOffset)
continue;
// If the Sibling ends before the Current starts, they don't overlap
// and we dont compact them.
if (Sibling.EndOffset <= Current.StartOffset)
continue;
// Here we have a strong evidence that Sibling overlaps the range
// of Current for at least one byte.
// At this point we want to find out if we can compact them.
// First of all we have to try and adjust the alignment of the array
// element, so that the elements of Current and Sibling are aligned.
// We can only do this by trying to move the start of an array element
// to a lower offset, but we have to try both to move Current and to
// move Sibling, and see which one gives the best results.
// Try to shift Current to a lower StartOffset, to match Sibling's
// alignment.
std::optional<CompactedArrayInfo>
FittedShiftingCurrent = tryShiftLowerAndCompact(Current, Sibling);
// Try to shift Sibling to a lower StartOffset, to match Current's
// alignment.
std::optional<CompactedArrayInfo>
FittedShiftingSibling = tryShiftLowerAndCompact(Sibling, Current);
std::optional<CompactedArrayInfo>
&MaybeBest = pickBest(FittedShiftingCurrent, FittedShiftingSibling);
// If both adjustments failed, we give up on trying to compact Current
// and Sibling, and skip to the next sibling.
if (not MaybeBest.has_value())
continue;
CompactedArrayInfo &Best = MaybeBest.value();
// If we're moving the StartOffset backward (or the EndOffset
// forward) the size of the range we're tracking is going to increase.
// We have to restart looking at array siblings because, given that
// the range has increased, there could be new siblings that have
// overlapping bytes, and we have to take them into consideration too.
if (Best.StartOffset < Current.StartOffset
or Best.EndOffset > Current.EndOffset)
SiblingEdgeNext = GT::child_edge_begin(Parent);
Current = Best;
// Here we know that ArraySibling can be compacted with the current
// array we're tracking.
CompactedWithCurrent.insert(SiblingEdgeIt);
}
return Current;
}
bool CompactCompatibleArrays::runOnTypeSystem(LayoutTypeSystem &TS) {
bool Changed = false;
if (VerifyLog.isEnabled())
revng_assert(TS.verifyDAG());
std::set<LayoutTypeSystemNode *> Visited;
for (LayoutTypeSystemNode *Root : llvm::nodes(&TS)) {
if (not isRoot(Root))
continue;
using NonPointerFilter = EdgeFilteredGraph<LayoutTypeSystemNode *,
isNotPointerEdge>;
for (LayoutTypeSystemNode *Parent :
llvm::post_order_ext(NonPointerFilter(Root), Visited)) {
// Skip leaf nodes (pointer nodes and access nodes).
if (isLeaf(Parent))
continue;
auto ChildEdgeIt = GT::child_edge_begin(Parent);
auto ChildEdgeNext = ChildEdgeIt;
auto ChildEdgeEnd = GT::child_edge_end(Parent);
for (; ChildEdgeIt != ChildEdgeEnd; ChildEdgeIt = ChildEdgeNext) {
ChildEdgeNext = std::next(ChildEdgeIt);
// Ignore edges that are not strided.
const auto &ArrayEdge = *ChildEdgeIt;
if (not isStridedInstance(ArrayEdge))
continue;
// Here we're starting from ArrayEdge, which represents an array that
// has not been grouped with others yet.
// Now we want to look around ArrayEdge, and see if there are other
// conflicting siblings that may need to be compacted with ArrayEdge
// itself, possibly changing the trip count or the actual start of the
// array.
// Let's compute the running variables that we'll use to track where the
// compacted array starts and ends.
// These will be updated in flight until we finish the group and decide
// all the siblings that need to be compacted with ArrayEdge.
CompactedArrayInfo Current = makeCompactedArrayInfo(ArrayEdge);
// Ok, now we start looking at other array siblings of ArrayEdge.
// If we find an ArraySibling that strongly overlaps with the array
// we're tracking we compact them and update our Current.
CompactedEdgeVector CompactedWithCurrent = {};
CompactedWithCurrent.insert(ChildEdgeIt);
// Compact with strided edges first.
Current = getEdgeToCompactWithCurrent<true>(Parent,
Current,
CompactedWithCurrent);
// Then compact with non-strided instance edges if it's still possible.
Current = getEdgeToCompactWithCurrent<false>(Parent,
Current,
CompactedWithCurrent);
// If we have something to compact, do it
if (CompactedWithCurrent.size() > 1) {
Changed = true;
// New artificial node representing an element of the compacted array
auto *New = TS.createArtificialLayoutType();
New->Size = Current.AccessedElementSize;
// Helper lambda to compact the various components into the compacted
// array.
auto Compact = [&](const NeighborIterator &ToCompactIt) {
auto &[TargetNode, EdgeTag] = *ToCompactIt;
uint64_t OldOffset = EdgeTag->getOffsetExpr().Offset;
revng_assert(OldOffset >= Current.StartOffset);
uint64_t OffsetInArray = (OldOffset - Current.StartOffset)
% Current.Stride;
TS.addInstanceLink(New,
TargetNode,
OffsetExpression{ OffsetInArray });
return TS.eraseEdge(Parent, ToCompactIt);
};
// Compact all the array components.
const auto VectorToCompact = CompactedWithCurrent.takeVector();
revng_assert(VectorToCompact.front() == ChildEdgeIt);
for (const NeighborIterator &ToCompact :
llvm::drop_begin(VectorToCompact))
Compact(ToCompact);
// We compact ChildEdgeIt as last, so that it updates ChildEdgeNext
// properly to continue the outer iteration.
ChildEdgeNext = Compact(ChildEdgeIt);
OffsetExpression NewStridedOffset{ Current.StartOffset };
NewStridedOffset.Strides.push_back(Current.Stride);
NewStridedOffset.TripCounts
.push_back(getTripCount(Current.StartOffset,
Current.EndOffset,
Current.Stride));
TS.addInstanceLink(Parent, New, std::move(NewStridedOffset));
} else {
revng_assert(CompactedWithCurrent.size() == 1);
revng_assert(CompactedWithCurrent.front() == ChildEdgeIt);
}
}
}
}
if (VerifyLog.isEnabled())
revng_assert(TS.verifyDAG());
return Changed;
}
} // end namespace dla