//===-- 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(); const W65816InstrInfo *TII = STI.getInstrInfo(); const TargetRegisterInfo *TRI = STI.getRegisterInfo(); bool Changed = false; // Slots whose last reference we erased — candidates for reclamation. SmallSet SlotsTouched; for (auto &MBB : MF) { // Pass 1: collect (STAfi, slot) entries. SmallVector, 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; }