65816-llvm-mos/src/llvm/lib/Target/W65816/W65816SpillToX.cpp
Scott Duensing 6d7eae0356 Checkpoint.
2026-04-30 01:29:16 -05:00

365 lines
14 KiB
C++

//===-- W65816SpillToX.cpp - Replace stack spills with TAX/TXA -----------===//
//
// 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
//
//===----------------------------------------------------------------------===//
//
// Post-RA peephole: replace stack-spill/reload pairs with TAX/TXA (or
// TAY/TYA) when the index register is dead during the spill window.
//
// Fast regalloc spills A to stack via STAfi/LDAfi, costing ~12 cycles
// per round-trip (sta is 5 cycles + lda is 5 cycles + the displacement
// dispatch). But the W65816 has TAX (2 cycles) + TXA (2 cycles), a
// 3x speedup if X is free during the spill window.
//
// We scan each basic block for the pattern:
//
// STAfi $a, slot, 0
// ... (instructions that don't touch X or A's slot, don't kill A)
// LDAfi $a, slot, 0
//
// If no instruction in the gap reads or writes X (or P-flags-dependent
// X side effects, etc.), we rewrite the pair as:
//
// TAX
// ...
// TXA
//
// This saves 4 bytes (stack-rel addressing is 2 bytes per op vs TAX/TXA
// at 1 byte each) AND saves the memory traffic. Net: ~8 cycles per
// converted pair.
//
// Conservative liveness: we treat X as "in use" if ANY instruction in
// the gap references W65816::X (def or use). False positives mean
// we keep the slow stack form; false negatives are correctness bugs.
//
//===----------------------------------------------------------------------===//
#include "W65816.h"
#include "W65816InstrInfo.h"
#include "W65816Subtarget.h"
#include "llvm/ADT/SmallSet.h"
#include "llvm/CodeGen/MachineFrameInfo.h"
#include "llvm/CodeGen/MachineFunction.h"
#include "llvm/CodeGen/MachineFunctionPass.h"
#include "llvm/CodeGen/MachineInstr.h"
#include "llvm/CodeGen/MachineInstrBuilder.h"
#include "llvm/CodeGen/TargetRegisterInfo.h"
using namespace llvm;
#define DEBUG_TYPE "w65816-spill-to-x"
namespace {
class W65816SpillToX : public MachineFunctionPass {
public:
static char ID;
W65816SpillToX() : MachineFunctionPass(ID) {}
StringRef getPassName() const override {
return "W65816 spill-to-X peephole";
}
void getAnalysisUsage(AnalysisUsage &AU) const override {
AU.setPreservesCFG();
MachineFunctionPass::getAnalysisUsage(AU);
}
bool runOnMachineFunction(MachineFunction &MF) override;
};
} // namespace
char W65816SpillToX::ID = 0;
INITIALIZE_PASS(W65816SpillToX, DEBUG_TYPE, "W65816 spill-to-X peephole",
false, false)
FunctionPass *llvm::createW65816SpillToX() {
return new W65816SpillToX();
}
// Classifies how an MI interacts with X.
enum XEffect { XNone = 0, XReads = 1, XDefs = 2, XBoth = 3 };
// Most W65816 transfer/index opcodes (TAX, INX, LDX, STX, CPX, etc.)
// are tablegen'd as `InstImplied` with no Defs/Uses metadata, so the
// MCInstrDesc carries no implicit X operand and a generic operand
// scan misses them. We hard-code the X-effect per opcode instead.
// Calls clobber X under our caller-saved-X ABI.
static XEffect xEffect(const MachineInstr &MI, const TargetRegisterInfo *TRI) {
switch (MI.getOpcode()) {
case W65816::TAX: // X := A
case W65816::TYX: // X := Y
case W65816::TSX: // X := SP
case W65816::PLX: // X := pop
return XDefs;
case W65816::TXA: // A := X
case W65816::TXY: // Y := X
case W65816::TXS: // SP := X
case W65816::PHX: // push X
return XReads;
case W65816::INX: // X := X+1
case W65816::DEX: // X := X-1
return XBoth;
default:
break;
}
if (MI.isCall()) return XBoth; // caller-clobbered X
// Generic operand scan for opcodes that carry X explicitly (LDX/STX/CPX
// pseudos) or any properly-modelled implicit defs/uses.
int eff = XNone;
for (const auto &MO : MI.operands()) {
if (!MO.isReg()) continue;
Register R = MO.getReg();
if (!R.isPhysical()) continue;
bool isX = R == W65816::X || (TRI && TRI->regsOverlap(R, W65816::X));
if (!isX) continue;
if (MO.isDef()) eff |= XDefs; else eff |= XReads;
}
return (XEffect)eff;
}
// Convenience wrapper: returns true if MI references X in any way.
static bool touchesX(const MachineInstr &MI, const TargetRegisterInfo *TRI) {
return xEffect(MI, TRI) != XNone;
}
// Returns true if MI is `STAfi $a, slot, 0`.
static int matchSTAfi(const MachineInstr &MI) {
if (MI.getOpcode() != W65816::STAfi) return -1;
if (MI.getNumOperands() < 3) return -1;
if (!MI.getOperand(0).isReg() || MI.getOperand(0).getReg() != W65816::A)
return -1;
if (!MI.getOperand(1).isFI()) return -1;
if (!MI.getOperand(2).isImm() || MI.getOperand(2).getImm() != 0) return -1;
return MI.getOperand(1).getIndex();
}
// Returns FI if MI is `LDAfi slot, 0` defining $a, else -1.
static int matchLDAfi(const MachineInstr &MI) {
if (MI.getOpcode() != W65816::LDAfi) return -1;
if (MI.getNumOperands() < 3) return -1;
if (!MI.getOperand(0).isReg() || MI.getOperand(0).getReg() != W65816::A)
return -1;
if (!MI.getOperand(1).isFI()) return -1;
if (!MI.getOperand(2).isImm() || MI.getOperand(2).getImm() != 0) return -1;
return MI.getOperand(1).getIndex();
}
// Returns true if MI reads or writes the slot at FrameIndex FI.
static bool referencesSlot(const MachineInstr &MI, int FI) {
for (const auto &MO : MI.operands()) {
if (MO.isFI() && MO.getIndex() == FI) return true;
}
return false;
}
bool W65816SpillToX::runOnMachineFunction(MachineFunction &MF) {
const W65816Subtarget &STI = MF.getSubtarget<W65816Subtarget>();
const W65816InstrInfo *TII = STI.getInstrInfo();
const TargetRegisterInfo *TRI = STI.getRegisterInfo();
bool Changed = false;
// Slots whose last reference we erased — candidates for reclamation.
SmallSet<int, 8> SlotsTouched;
for (auto &MBB : MF) {
// Pass 1: collect (STAfi, slot) entries.
SmallVector<std::pair<MachineInstr *, int>, 8> Stas;
for (auto &MI : MBB) {
int FI = matchSTAfi(MI);
if (FI != -1) Stas.push_back({&MI, FI});
}
// For each STAfi, scan forward for the matching LDAfi with no
// intervening X touch or slot reference. Process in REVERSE
// order so any nested pair is converted first; the outer pair's
// gap scan then sees the inner TAX/TXA (which touches X) and
// bails — preventing a mid-bridge X clobber.
for (auto It = Stas.rbegin(); It != Stas.rend(); ++It) {
auto [StaMI, FI] = *It;
bool xTouched = false;
bool gapEmpty = true;
MachineInstr *LdaMI = nullptr;
for (auto Scan = std::next(MachineBasicBlock::iterator(StaMI));
Scan != MBB.end(); ++Scan) {
MachineInstr &MI2 = *Scan;
if (MI2.isDebugInstr()) continue;
// Look for the matching LDAfi. TAX preserves A so we don't
// need to check A liveness — only whether X was free.
if (matchLDAfi(MI2) == FI) { LdaMI = &MI2; break; }
// Bail if X is touched (use or def, including implicit on
// calls) or if the slot is referenced by something else
// (which would invalidate the saved value).
if (touchesX(MI2, TRI)) { xTouched = true; break; }
if (referencesSlot(MI2, FI)) break;
gapEmpty = false;
}
// Defer empty-gap pairs to StackSlotCleanup, which deletes both
// (A still holds the stored value across an empty gap). That
// beats our TAX+TXA conversion (0 instr vs 2 instr).
if (!LdaMI || xTouched || gapEmpty) continue;
// X-live-after-LDA check: TXA (the LDAfi replacement) clobbers X.
// If anything downstream of the LDA reads X — including the next
// JSL's implicit $x — then we'd silently corrupt X. Caught by
// i32 first-arg functions where $x is live-in (= arg0_hi) and
// a libcall later in the block expects $x intact. Scan from just
// past LDA to end-of-block; if any instr uses X, bail.
bool xUsedAfter = false;
for (auto Scan = std::next(MachineBasicBlock::iterator(LdaMI));
Scan != MBB.end(); ++Scan) {
const MachineInstr &MI3 = *Scan;
if (MI3.isDebugInstr()) continue;
XEffect eff = xEffect(MI3, TRI);
if (eff & XReads) { xUsedAfter = true; break; }
if (eff & XDefs) break; // X redefined; no longer live
}
// Also bail if X is live-in to MBB and nothing has defined X
// between MBB start and STA — the live-in value is needed past
// the LDA point.
if (!xUsedAfter && MBB.isLiveIn(W65816::X)) {
bool xRedefBeforeSta = false;
for (auto Scan = MBB.begin();
Scan != MachineBasicBlock::iterator(StaMI); ++Scan) {
const MachineInstr &MI3 = *Scan;
if (MI3.isDebugInstr()) continue;
if (xEffect(MI3, TRI) & XDefs) { xRedefBeforeSta = true; break; }
}
if (!xRedefBeforeSta) xUsedAfter = true;
}
if (xUsedAfter) continue;
// Cross-block use check: if the slot is referenced anywhere
// OUTSIDE the [STA, LDA] window (including other blocks), the
// STA we'd erase is feeding those other reads — eliding it
// would silently corrupt them. Caught by sumTable() returning
// a stale phi value because the loop's STA-to-merge-slot was
// eliminated; the merge block's LDA then read the bb.0-init 0
// instead of the loop's accumulated sum.
bool externalUse = false;
for (auto &OtherMBB : MF) {
for (auto &OtherMI : OtherMBB) {
if (&OtherMI == StaMI || &OtherMI == LdaMI) continue;
// Walk inside-window range and skip those refs.
if (&OtherMBB == &MBB) {
// We already verified the gap doesn't reference FI; only
// STA/LDA themselves are allowed users in this block.
}
if (referencesSlot(OtherMI, FI)) {
externalUse = true;
break;
}
}
if (externalUse) break;
}
if (externalUse) continue;
// Replace STAfi with TAX, LDAfi with TXA.
DebugLoc StaDL = StaMI->getDebugLoc();
DebugLoc LdaDL = LdaMI->getDebugLoc();
MachineBasicBlock *MBB2 = StaMI->getParent();
auto StaIt = MachineBasicBlock::iterator(StaMI);
auto LdaIt = MachineBasicBlock::iterator(LdaMI);
BuildMI(*MBB2, StaIt, StaDL, TII->get(W65816::TAX));
BuildMI(*MBB2, LdaIt, LdaDL, TII->get(W65816::TXA))
.addReg(W65816::A, RegState::ImplicitDefine);
StaMI->eraseFromParent();
LdaMI->eraseFromParent();
SlotsTouched.insert(FI);
Changed = true;
}
// Post-pass: collapse `TAX ; TXA` (or `TXA ; TAX`) pairs whose
// observable effect is dead. These appear when an inner STA/LDA
// pair (originally between an outer pair we converted) was deleted
// by StackSlotCleanup or coalesced by stack-slot-coloring, leaving
// our TAX/TXA bookends adjacent.
//
// Distinct effect per ordering:
// TAX;TXA : net effect is `X := A` (A unchanged, X clobbered).
// Removable iff X dead afterwards.
// TXA;TAX : net effect is `A := X` (X unchanged, A clobbered).
// Removable iff A dead afterwards.
//
// The earlier code mis-handled TXA;TAX as if it clobbered X; in
// fact X comes through the pair unchanged.
auto It = MBB.begin();
while (It != MBB.end()) {
auto Next = std::next(It);
if (Next == MBB.end()) break;
bool isTaxThenTxa = It->getOpcode() == W65816::TAX &&
Next->getOpcode() == W65816::TXA;
bool isTxaThenTax = It->getOpcode() == W65816::TXA &&
Next->getOpcode() == W65816::TAX;
if (!isTaxThenTxa && !isTxaThenTax) { ++It; continue; }
// Choose which physreg's liveness matters based on which value
// the pair clobbers.
Register Clobbered = isTaxThenTxa ? W65816::X : W65816::A;
bool observed = false;
bool killedByDef = false;
for (auto Tail = std::next(Next); Tail != MBB.end(); ++Tail) {
if (Tail->isDebugInstr()) continue;
if (Tail->readsRegister(Clobbered, TRI)) { observed = true; break; }
// Calls clobber both A and X (caller-saved).
if (Tail->isCall()) { killedByDef = true; break; }
// Opcode-based defs (TAX/TXA tablegen has no Defs metadata).
if (Clobbered == W65816::X) {
XEffect E = xEffect(*Tail, TRI);
if (E & XReads) { observed = true; break; }
if (E & XDefs) { killedByDef = true; break; }
} else {
// For A: any LDA*/PLA/TXA/TYA/INA/DEA/arith op redefines A.
unsigned Op = Tail->getOpcode();
if (Op == W65816::TXA || Op == W65816::TYA ||
Op == W65816::INA || Op == W65816::DEA ||
Op == W65816::PLA) { killedByDef = true; break; }
if (Tail->modifiesRegister(W65816::A, TRI)) {
killedByDef = true; break;
}
}
}
if (observed) { ++It; continue; }
if (!killedByDef) {
bool liveOut = false;
for (MachineBasicBlock *Succ : MBB.successors()) {
if (Succ->isLiveIn(Clobbered)) { liveOut = true; break; }
}
if (liveOut) { ++It; continue; }
}
auto Erase1 = It++;
auto Erase2 = It++;
Erase1->eraseFromParent();
Erase2->eraseFromParent();
Changed = true;
}
}
// Reclaim frame slots whose last reference we just erased. Without
// this, PEI still allocates space for them and emits the prologue
// PHA, even though the slot is unused — wastes 1 PHA (4 cyc) and
// 1 PLY per call. RemoveStackObject marks the slot dead by setting
// its size to ~0ULL; PEI ignores those when computing frame size.
if (!SlotsTouched.empty()) {
MachineFrameInfo &MFI = MF.getFrameInfo();
for (int FI : SlotsTouched) {
bool stillUsed = false;
for (auto &MBB : MF) {
for (auto &MI : MBB) {
if (referencesSlot(MI, FI)) { stillUsed = true; break; }
}
if (stillUsed) break;
}
if (!stillUsed) MFI.RemoveStackObject(FI);
}
}
return Changed;
}