André Rösti abda8ce2ee
llvm-mca: Disentangle MemoryGroup from LSUnitBase (#114159)
In MCA, the load/store unit is modeled through a `LSUnitBase` class.
Judging from the name `LSUnitBase`, I believe there is an intent to
allow for different specialized load/store unit implementations.
(However, currently there is only one implementation used in-tree,
`LSUnit`.)

PR #101534 fixed one instance where the specialized `LSUnit` was
hard-coded, opening the door for other subclasses to be used, but what
subclasses can do is, in my opinion, still overly limited due to a
reliance on the `MemoryGroup` class, e.g.
[here](8b55162e19/llvm/lib/MCA/HardwareUnits/Scheduler.cpp (L88)).

The `MemoryGroup` class is currently used in the default `LSUnit`
implementation to model data dependencies/hazards in the pipeline.
`MemoryGroups` form a graph of memory dependencies that inform the
scheduler when load/store instructions can be executed relative to each
other.

In my eyes, this is an implementation detail. Other `LSUnit`s may want
to keep track of data dependencies in different ways. As a concrete
example, a downstream use I am working on<sup>[1]</sup> uses a custom
load/store unit that makes use of available aliasing information. I
haven't been able to shoehorn our additional aliasing information into
the existing `MemoryGroup` abstraction. I think there is no need to
force subclasses to use `MemoryGroup`s; users of `LSUnitBase` are only
concerned with when, and for how long, a load/store instruction
executes.

This PR makes changes to instead leave it up to the subclasses how to
model such dependencies, and only prescribes an abstract interface in
`LSUnitBase`. It also moves data members and methods that are not
necessary to provide an abstract interface from `LSUnitBase` to the
`LSUnit` subclass. I decided to make the `MemoryGroup` a protected
subclass of `LSUnit`; that way, specializations may inherit from
`LSUnit` and still make use of `MemoryGroup`s if they wish to do so
(e.g. if they want to only overwrite the `dispatch` method).

**Drawbacks / Considerations**

My reason for suggesting this PR is an out-of-tree use. As such, these
changes don't introduce any new functionality for in-tree LLVM uses.
However, in my opinion, these changes improve code clarity and prescribe
a clear interface, which would be the main benefit for the LLVM
community.

A drawback of the more abstract interface is that virtual dispatching is
used in more places. However, note that virtual dispatch is already
currently used in some critical parts of the `LSUnitBase`, e.g. the
`isAvailable` and `dispatch` methods. As a quick check to ensure these
changes don't significantly negatively impact performance, I also ran
`time llvm-mca -mtriple=x86_64-unknown-unknown -mcpu=btver2
-iterations=3000 llvm/test/tools/llvm-mca/X86/BtVer2/dot-product.s`
before and after the changes; there was no observable difference in
runtimes (`0.292 s` total before, `0.286 s` total after changes).

<sup>[1]: MCAD started by @mshockwave and @chinmaydd.</sup>
2024-11-18 14:36:17 +00:00

343 lines
11 KiB
C++

//===--------------------- Scheduler.cpp ------------------------*- C++ -*-===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
//
// A scheduler for processor resource units and processor resource groups.
//
//===----------------------------------------------------------------------===//
#include "llvm/MCA/HardwareUnits/Scheduler.h"
#include "llvm/Support/Debug.h"
#include "llvm/Support/raw_ostream.h"
namespace llvm {
namespace mca {
#define DEBUG_TYPE "llvm-mca"
void Scheduler::initializeStrategy(std::unique_ptr<SchedulerStrategy> S) {
// Ensure we have a valid (non-null) strategy object.
Strategy = S ? std::move(S) : std::make_unique<DefaultSchedulerStrategy>();
}
// Anchor the vtable of SchedulerStrategy and DefaultSchedulerStrategy.
SchedulerStrategy::~SchedulerStrategy() = default;
DefaultSchedulerStrategy::~DefaultSchedulerStrategy() = default;
#ifndef NDEBUG
void Scheduler::dump() const {
dbgs() << "[SCHEDULER]: WaitSet size is: " << WaitSet.size() << '\n';
dbgs() << "[SCHEDULER]: ReadySet size is: " << ReadySet.size() << '\n';
dbgs() << "[SCHEDULER]: IssuedSet size is: " << IssuedSet.size() << '\n';
Resources->dump();
}
#endif
Scheduler::Status Scheduler::isAvailable(const InstRef &IR) {
ResourceStateEvent RSE =
Resources->canBeDispatched(IR.getInstruction()->getUsedBuffers());
HadTokenStall = RSE != RS_BUFFER_AVAILABLE;
switch (RSE) {
case ResourceStateEvent::RS_BUFFER_UNAVAILABLE:
return Scheduler::SC_BUFFERS_FULL;
case ResourceStateEvent::RS_RESERVED:
return Scheduler::SC_DISPATCH_GROUP_STALL;
case ResourceStateEvent::RS_BUFFER_AVAILABLE:
break;
}
// Give lower priority to LSUnit stall events.
LSUnit::Status LSS = LSU.isAvailable(IR);
HadTokenStall = LSS != LSUnit::LSU_AVAILABLE;
switch (LSS) {
case LSUnit::LSU_LQUEUE_FULL:
return Scheduler::SC_LOAD_QUEUE_FULL;
case LSUnit::LSU_SQUEUE_FULL:
return Scheduler::SC_STORE_QUEUE_FULL;
case LSUnit::LSU_AVAILABLE:
return Scheduler::SC_AVAILABLE;
}
llvm_unreachable("Don't know how to process this LSU state result!");
}
void Scheduler::issueInstructionImpl(
InstRef &IR,
SmallVectorImpl<std::pair<ResourceRef, ReleaseAtCycles>> &UsedResources) {
Instruction *IS = IR.getInstruction();
const InstrDesc &D = IS->getDesc();
// Issue the instruction and collect all the consumed resources
// into a vector. That vector is then used to notify the listener.
Resources->issueInstruction(D, UsedResources);
// Notify the instruction that it started executing.
// This updates the internal state of each write.
IS->execute(IR.getSourceIndex());
IS->computeCriticalRegDep();
if (IS->isMemOp()) {
LSU.onInstructionIssued(IR);
const CriticalDependency &MemDep =
LSU.getCriticalPredecessor(IS->getLSUTokenID());
IS->setCriticalMemDep(MemDep);
}
if (IS->isExecuting())
IssuedSet.emplace_back(IR);
else if (IS->isExecuted())
LSU.onInstructionExecuted(IR);
}
// Release the buffered resources and issue the instruction.
void Scheduler::issueInstruction(
InstRef &IR,
SmallVectorImpl<std::pair<ResourceRef, ReleaseAtCycles>> &UsedResources,
SmallVectorImpl<InstRef> &PendingInstructions,
SmallVectorImpl<InstRef> &ReadyInstructions) {
const Instruction &Inst = *IR.getInstruction();
bool HasDependentUsers = Inst.hasDependentUsers();
HasDependentUsers |= Inst.isMemOp() && LSU.hasDependentUsers(IR);
Resources->releaseBuffers(Inst.getUsedBuffers());
issueInstructionImpl(IR, UsedResources);
// Instructions that have been issued during this cycle might have unblocked
// other dependent instructions. Dependent instructions may be issued during
// this same cycle if operands have ReadAdvance entries. Promote those
// instructions to the ReadySet and notify the caller that those are ready.
if (HasDependentUsers)
if (promoteToPendingSet(PendingInstructions))
promoteToReadySet(ReadyInstructions);
}
bool Scheduler::promoteToReadySet(SmallVectorImpl<InstRef> &Ready) {
// Scan the set of waiting instructions and promote them to the
// ready set if operands are all ready.
unsigned PromotedElements = 0;
for (auto I = PendingSet.begin(), E = PendingSet.end(); I != E;) {
InstRef &IR = *I;
if (!IR)
break;
// Check if there are unsolved register dependencies.
Instruction &IS = *IR.getInstruction();
if (!IS.isReady() && !IS.updatePending()) {
++I;
continue;
}
// Check if there are unsolved memory dependencies.
if (IS.isMemOp() && !LSU.isReady(IR)) {
++I;
continue;
}
LLVM_DEBUG(dbgs() << "[SCHEDULER]: Instruction #" << IR
<< " promoted to the READY set.\n");
Ready.emplace_back(IR);
ReadySet.emplace_back(IR);
IR.invalidate();
++PromotedElements;
std::iter_swap(I, E - PromotedElements);
}
PendingSet.resize(PendingSet.size() - PromotedElements);
return PromotedElements;
}
bool Scheduler::promoteToPendingSet(SmallVectorImpl<InstRef> &Pending) {
// Scan the set of waiting instructions and promote them to the
// pending set if operands are all ready.
unsigned RemovedElements = 0;
for (auto I = WaitSet.begin(), E = WaitSet.end(); I != E;) {
InstRef &IR = *I;
if (!IR)
break;
// Check if this instruction is now ready. In case, force
// a transition in state using method 'updateDispatched()'.
Instruction &IS = *IR.getInstruction();
if (IS.isDispatched() && !IS.updateDispatched()) {
++I;
continue;
}
if (IS.isMemOp() && LSU.isWaiting(IR)) {
++I;
continue;
}
LLVM_DEBUG(dbgs() << "[SCHEDULER]: Instruction #" << IR
<< " promoted to the PENDING set.\n");
Pending.emplace_back(IR);
PendingSet.emplace_back(IR);
IR.invalidate();
++RemovedElements;
std::iter_swap(I, E - RemovedElements);
}
WaitSet.resize(WaitSet.size() - RemovedElements);
return RemovedElements;
}
InstRef Scheduler::select() {
unsigned QueueIndex = ReadySet.size();
for (unsigned I = 0, E = ReadySet.size(); I != E; ++I) {
InstRef &IR = ReadySet[I];
if (QueueIndex == ReadySet.size() ||
Strategy->compare(IR, ReadySet[QueueIndex])) {
Instruction &IS = *IR.getInstruction();
uint64_t BusyResourceMask = Resources->checkAvailability(IS.getDesc());
if (BusyResourceMask)
IS.setCriticalResourceMask(BusyResourceMask);
BusyResourceUnits |= BusyResourceMask;
if (!BusyResourceMask)
QueueIndex = I;
}
}
if (QueueIndex == ReadySet.size())
return InstRef();
// We found an instruction to issue.
InstRef IR = ReadySet[QueueIndex];
std::swap(ReadySet[QueueIndex], ReadySet[ReadySet.size() - 1]);
ReadySet.pop_back();
return IR;
}
void Scheduler::updateIssuedSet(SmallVectorImpl<InstRef> &Executed) {
unsigned RemovedElements = 0;
for (auto I = IssuedSet.begin(), E = IssuedSet.end(); I != E;) {
InstRef &IR = *I;
if (!IR)
break;
Instruction &IS = *IR.getInstruction();
if (!IS.isExecuted()) {
LLVM_DEBUG(dbgs() << "[SCHEDULER]: Instruction #" << IR
<< " is still executing.\n");
++I;
continue;
}
// Instruction IR has completed execution.
LSU.onInstructionExecuted(IR);
Executed.emplace_back(IR);
++RemovedElements;
IR.invalidate();
std::iter_swap(I, E - RemovedElements);
}
IssuedSet.resize(IssuedSet.size() - RemovedElements);
}
uint64_t Scheduler::analyzeResourcePressure(SmallVectorImpl<InstRef> &Insts) {
llvm::append_range(Insts, ReadySet);
return BusyResourceUnits;
}
void Scheduler::analyzeDataDependencies(SmallVectorImpl<InstRef> &RegDeps,
SmallVectorImpl<InstRef> &MemDeps) {
const auto EndIt = PendingSet.end() - NumDispatchedToThePendingSet;
for (const InstRef &IR : make_range(PendingSet.begin(), EndIt)) {
const Instruction &IS = *IR.getInstruction();
if (Resources->checkAvailability(IS.getDesc()))
continue;
if (IS.isMemOp() && LSU.isPending(IR))
MemDeps.emplace_back(IR);
if (IS.isPending())
RegDeps.emplace_back(IR);
}
}
void Scheduler::cycleEvent(SmallVectorImpl<ResourceRef> &Freed,
SmallVectorImpl<InstRef> &Executed,
SmallVectorImpl<InstRef> &Pending,
SmallVectorImpl<InstRef> &Ready) {
LSU.cycleEvent();
// Release consumed resources.
Resources->cycleEvent(Freed);
for (InstRef &IR : IssuedSet)
IR.getInstruction()->cycleEvent();
updateIssuedSet(Executed);
for (InstRef &IR : PendingSet)
IR.getInstruction()->cycleEvent();
for (InstRef &IR : WaitSet)
IR.getInstruction()->cycleEvent();
promoteToPendingSet(Pending);
promoteToReadySet(Ready);
NumDispatchedToThePendingSet = 0;
BusyResourceUnits = 0;
}
bool Scheduler::mustIssueImmediately(const InstRef &IR) const {
const InstrDesc &Desc = IR.getInstruction()->getDesc();
if (Desc.isZeroLatency())
return true;
// Instructions that use an in-order dispatch/issue processor resource must be
// issued immediately to the pipeline(s). Any other in-order buffered
// resources (i.e. BufferSize=1) is consumed.
return Desc.MustIssueImmediately;
}
bool Scheduler::dispatch(InstRef &IR) {
Instruction &IS = *IR.getInstruction();
Resources->reserveBuffers(IS.getUsedBuffers());
// If necessary, reserve queue entries in the load-store unit (LSU).
if (IS.isMemOp())
IS.setLSUTokenID(LSU.dispatch(IR));
if (IS.isDispatched() || (IS.isMemOp() && LSU.isWaiting(IR))) {
LLVM_DEBUG(dbgs() << "[SCHEDULER] Adding #" << IR << " to the WaitSet\n");
WaitSet.push_back(IR);
return false;
}
if (IS.isPending() || (IS.isMemOp() && LSU.isPending(IR))) {
LLVM_DEBUG(dbgs() << "[SCHEDULER] Adding #" << IR
<< " to the PendingSet\n");
PendingSet.push_back(IR);
++NumDispatchedToThePendingSet;
return false;
}
assert(IS.isReady() && (!IS.isMemOp() || LSU.isReady(IR)) &&
"Unexpected internal state found!");
// Don't add a zero-latency instruction to the Ready queue.
// A zero-latency instruction doesn't consume any scheduler resources. That is
// because it doesn't need to be executed, and it is often removed at register
// renaming stage. For example, register-register moves are often optimized at
// register renaming stage by simply updating register aliases. On some
// targets, zero-idiom instructions (for example: a xor that clears the value
// of a register) are treated specially, and are often eliminated at register
// renaming stage.
if (!mustIssueImmediately(IR)) {
LLVM_DEBUG(dbgs() << "[SCHEDULER] Adding #" << IR << " to the ReadySet\n");
ReadySet.push_back(IR);
}
return true;
}
} // namespace mca
} // namespace llvm