65816-llvm-mos/src/llvm/lib/Target/W65816/W65816PromoteFiToImg.cpp
Scott Duensing 6bff7bea3f Docs!
2026-05-14 11:23:00 -05:00

371 lines
15 KiB
C++

//===-- W65816PromoteFiToImg.cpp - Promote FrameIndex to IMG slot --------===//
//
// 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, pre-PEI pass. Counts accesses to each i16-sized FrameIndex
// in the function and rewrites the top-K hottest ones to use IMG8..15
// DP slots ($C0/$C2/.../$CE) instead. K = number of free IMG8..15
// slots (slots not already used by regalloc decisions).
//
// Why post-RA: at this point regalloc has decided which vregs live in
// physical registers vs spill slots. The spills appear as the FI
// pseudo-opcodes (LDAfi/STAfi/ADCfi/SBCfi/ANDfi/ORAfi/EORfi/CMPfi),
// and the MFI tells us each FI's final size. We see all the accesses
// and can safely rewrite — eliminateFrameIndex hasn't yet baked the
// offsets into SP-relative immediates.
//
// Why before W65816ImgCalleeSave: ImgCalleeSave scans the post-PromoteFi
// MIR for IMG8..15 usage and emits prologue PHA-bracketed saves +
// epilogue restores for each used slot. Our promotion introduces
// fresh IMG8..15 references that ImgCalleeSave will then auto-cover.
//
// Per-access cost change:
// STAfi → STA_DP : 5 cyc / 3 B → 4 cyc / 2 B (saves 1 cyc/1 B)
// LDAfi → LDA_DP : 5 cyc / 3 B → 4 cyc / 2 B (saves 1 cyc/1 B)
// ADCfi → ADC_DP : 5 cyc / 3 B → 4 cyc / 2 B (saves 1 cyc/1 B)
// Per-slot one-time overhead (added by ImgCalleeSave):
// prologue save : ~10 cyc / 6 B
// epilogue restore: ~10 cyc / 6 B
// Net win if access_count * 1 > 20. Threshold is 5 to leave margin.
//
// Restrictions:
// - Only i16-sized FIs (2 bytes, offset 0). Larger slots (i32 halves,
// structs) are skipped.
// - Skips fixed/variable-sized objects.
// - Skips STA8fi (byte store needs SEP/REP wrap incompatible with
// simple STA_DP — and DP stores 16 bits in M=0).
// - Skips LDAfi_indY / STAfi_indY (indirect-Y form — different
// addressing).
//
//===---------------------------------------------------------------------===//
#include "W65816.h"
#include "W65816InstrInfo.h"
#include "W65816Subtarget.h"
#include "llvm/ADT/BitVector.h"
#include "llvm/ADT/DenseMap.h"
#include "llvm/CodeGen/MachineFrameInfo.h"
#include "llvm/CodeGen/MachineFunction.h"
#include "llvm/CodeGen/MachineFunctionPass.h"
#include "llvm/CodeGen/MachineInstrBuilder.h"
#include "llvm/CodeGen/MachineLoopInfo.h"
#include "llvm/CodeGen/MachineRegisterInfo.h"
#include "llvm/InitializePasses.h"
#include "llvm/Support/Debug.h"
#include "llvm/Support/Format.h"
using namespace llvm;
#define DEBUG_TYPE "w65816-promote-fi-to-img"
namespace {
class W65816PromoteFiToImg : public MachineFunctionPass {
public:
static char ID;
W65816PromoteFiToImg() : MachineFunctionPass(ID) {}
StringRef getPassName() const override {
return "W65816 promote FrameIndex to IMG8..15 DP slot";
}
void getAnalysisUsage(AnalysisUsage &AU) const override {
AU.addRequired<MachineLoopInfoWrapperPass>();
AU.setPreservesCFG();
MachineFunctionPass::getAnalysisUsage(AU);
}
bool runOnMachineFunction(MachineFunction &MF) override;
};
} // namespace
char W65816PromoteFiToImg::ID = 0;
INITIALIZE_PASS_BEGIN(W65816PromoteFiToImg, DEBUG_TYPE,
"W65816 promote FI to IMG", false, false)
INITIALIZE_PASS_DEPENDENCY(MachineLoopInfoWrapperPass)
INITIALIZE_PASS_END(W65816PromoteFiToImg, DEBUG_TYPE,
"W65816 promote FI to IMG", false, false)
FunctionPass *llvm::createW65816PromoteFiToImg() {
return new W65816PromoteFiToImg();
}
// Returns the operand index of the FrameIndex for the given FI pseudo
// opcode, or -1 if this opcode isn't a promotable FI carrier.
static int getFiOperandIdx(unsigned Opc) {
switch (Opc) {
case W65816::LDAfi: return 1;
case W65816::STAfi: return 1;
case W65816::CMPfi: return 1;
case W65816::ADCfi:
case W65816::SBCfi:
case W65816::ANDfi:
case W65816::ORAfi:
case W65816::EORfi: return 2;
default: return -1;
}
}
// Map a promotable FI pseudo to the corresponding DP MC opcode.
static unsigned getDpOpcode(unsigned Opc) {
switch (Opc) {
case W65816::LDAfi: return W65816::LDA_DP;
case W65816::STAfi: return W65816::STA_DP;
case W65816::CMPfi: return W65816::CMP_DP;
case W65816::ADCfi: return W65816::ADC_DP;
case W65816::SBCfi: return W65816::SBC_DP;
case W65816::ANDfi: return W65816::AND_DP;
case W65816::ORAfi: return W65816::ORA_DP;
case W65816::EORfi: return W65816::EOR_DP;
default: return 0;
}
}
// IMG8..IMG15 sit at DP addresses 0xC0, 0xC2, ..., 0xCE. IMG0..IMG7
// are at 0xD0..0xDE. Returns the DP byte for IMGn.
static uint8_t dpAddrForImg(unsigned ImgIdx) {
assert(ImgIdx < 16 && "IMG index out of range");
if (ImgIdx < 8) return 0xD0 + 2 * ImgIdx;
return 0xC0 + 2 * (ImgIdx - 8);
}
bool W65816PromoteFiToImg::runOnMachineFunction(MachineFunction &MF) {
// DISABLED again 2026-05-13 (3rd-attempt write-up). Two new findings:
// 1. With kMaxPromote=2 and IMG0..7 (caller-save, skip ImgCalleeSave),
// sumSquares regressed 56 → 72 inst because the FIs picked by
// access-count (fi#2, fi#3) are intermediate spill temps, not
// the i32-accumulator's halves (which are different FIs). The
// loop body ends up using BOTH IMG and stack slots for related
// values.
// 2. To pick the RIGHT FIs (those corresponding to PHI-cycled
// values like the i32 accumulator), we need either:
// (a) IR-level analysis BEFORE FI assignment, or
// (b) Post-RA dataflow analysis to identify "long-lived" FIs
// (active across the loop back-edge with no def/use boundary).
// This is the next blocker. Disabled until either (a) or (b) is
// implemented.
return false;
if (skipFunction(MF.getFunction())) return false;
const W65816Subtarget &STI = MF.getSubtarget<W65816Subtarget>();
const W65816InstrInfo *TII = STI.getInstrInfo();
MachineFrameInfo &MFI = MF.getFrameInfo();
// 1. Walk all instructions, count FI accesses for promotable opcodes.
// Weight by loop depth: an access inside a depth-N loop counts as
// 10^N to model the dynamic execution count (an inner-loop slot
// gets executed many times per outer call).
MachineLoopInfo &MLI =
getAnalysis<MachineLoopInfoWrapperPass>().getLI();
DenseMap<int, unsigned> AccessCount;
DenseMap<int, SmallVector<MachineInstr *, 8>> AccessSites;
for (MachineBasicBlock &MBB : MF) {
unsigned LoopDepth = MLI.getLoopDepth(&MBB);
unsigned Weight = 1;
for (unsigned i = 0; i < LoopDepth && i < 3; ++i) Weight *= 10;
for (MachineInstr &MI : MBB) {
int FiIdx = getFiOperandIdx(MI.getOpcode());
if (FiIdx < 0) continue;
const MachineOperand &MO = MI.getOperand(FiIdx);
if (!MO.isFI()) continue;
int FI = MO.getIndex();
if (MFI.isVariableSizedObjectIndex(FI)) continue;
if (MFI.getObjectSize(FI) != 2) continue;
if (FI < 0) continue;
const MachineOperand &OffMO = MI.getOperand(FiIdx + 1);
if (!OffMO.isImm() || OffMO.getImm() != 0) continue;
AccessCount[FI] += Weight;
AccessSites[FI].push_back(&MI);
}
}
if (AccessCount.empty()) return false;
// 2. Determine which IMG0..7 slots are already in use (caller-save).
// Use caller-save IMG0..7 instead of callee-save IMG8..15: this lets
// us skip ImgCalleeSave entirely (no prologue/epilogue overhead).
// The trade-off: any call inside the function clobbers IMG0..7. Mark
// any function with calls as "callees might clobber" → skip promotion.
// This restricts wins to leaf functions (no internal calls).
BitVector UsedImg(8, false);
for (MachineBasicBlock &MBB : MF) {
for (MachineInstr &MI : MBB) {
// Skip CALL instructions — their `implicit-def dead $img0..7`
// operand list marks every IMG slot used, but that's just the
// caller-save annotation, not actual value-bearing usage.
if (MI.isCall()) continue;
for (const MachineOperand &MO : MI.operands()) {
if (!MO.isReg() || !MO.getReg().isPhysical()) continue;
Register R = MO.getReg();
unsigned ImgIdx = 16;
if (R == W65816::IMG0) ImgIdx = 0;
else if (R == W65816::IMG1) ImgIdx = 1;
else if (R == W65816::IMG2) ImgIdx = 2;
else if (R == W65816::IMG3) ImgIdx = 3;
else if (R == W65816::IMG4) ImgIdx = 4;
else if (R == W65816::IMG5) ImgIdx = 5;
else if (R == W65816::IMG6) ImgIdx = 6;
else if (R == W65816::IMG7) ImgIdx = 7;
if (ImgIdx < 8) UsedImg.set(ImgIdx);
}
}
}
// 3. Sort FIs by access count (descending).
SmallVector<int, 16> Ordered;
for (auto &P : AccessCount) Ordered.push_back(P.first);
std::sort(Ordered.begin(), Ordered.end(),
[&](int A, int B) { return AccessCount[A] > AccessCount[B]; });
// 4. Assign IMG slots greedily. Each IMG8..15 slot used triggers
// a save/restore pair in W65816ImgCalleeSave (~20 cyc + ~12 B
// per slot per CALL into this function). For recursive or
// deep-call-stack functions, that overhead dominates the per-
// access savings — measured: promoting 4 slots in fib(10)
// regressed it 38% (12617 → 17391 cyc). Gate on a very high
// threshold + bail entirely if the function has any calls (the
// save/restore cost compounds with recursion / call frequency
// in ways the static access count can't capture).
bool HasCalls = false;
bool IsRecursive = false;
StringRef SelfName = MF.getName();
for (MachineBasicBlock &MBB : MF) {
for (MachineInstr &MI : MBB) {
if (MI.isCall()) {
HasCalls = true;
// Check for self-call (recursive).
for (const MachineOperand &MO : MI.operands()) {
if (MO.isGlobal() && MO.getGlobal()->getName() == SelfName)
IsRecursive = true;
else if (MO.isSymbol() && SelfName == MO.getSymbolName())
IsRecursive = true;
}
}
}
}
// Recursive functions: skip — recursion makes per-call overhead
// compound (each level of recursion pays the save/restore).
if (IsRecursive) return false;
// Caller-save IMG0..7 strategy: any internal call clobbers them, so
// the only safe promoted slots are those whose lifetime doesn't
// cross a call. For now, only promote in leaf functions (no internal
// calls at all). This catches simple loops like sumSquares (which
// calls __umulhisi3 — but that's in libgcc.s and doesn't actually
// touch IMG0..7; treat libgcc multiplies as IMG-safe).
//
// Whitelist of libgcc functions known to not touch IMG0..7.
auto isImgSafeLibcall = [](const MachineInstr &MI) -> bool {
if (!MI.isCall()) return false;
for (const MachineOperand &MO : MI.operands()) {
StringRef Name;
if (MO.isGlobal()) Name = MO.getGlobal()->getName();
else if (MO.isSymbol()) Name = MO.getSymbolName();
else continue;
// libgcc.s multiply/divide/shift helpers — verified to only use
// $E0..$E9 internally, no IMG0..7 touch.
if (Name == "__umulhisi3" || Name == "__mulhi3" ||
Name == "__mulsi3" || Name == "__udivhi3" ||
Name == "__umodhi3" || Name == "__divhi3" ||
Name == "__modhi3" || Name == "__udivsi3" ||
Name == "__umodsi3" || Name == "__divsi3" ||
Name == "__modsi3" || Name == "__ashlhi3" ||
Name == "__lshrhi3" || Name == "__ashrhi3" ||
Name == "__ashlsi3" || Name == "__lshrsi3" ||
Name == "__ashrsi3")
return true;
return false;
}
return false;
};
bool AllCallsImgSafe = true;
for (MachineBasicBlock &MBB : MF) {
for (MachineInstr &MI : MBB) {
if (MI.isCall() && !isImgSafeLibcall(MI)) {
AllCallsImgSafe = false;
break;
}
}
if (!AllCallsImgSafe) break;
}
if (HasCalls && !AllCallsImgSafe) return false;
// Threshold: per-access save is 1 cyc, no save/restore overhead. We
// just need the access count to be > 0 to win. Use a small threshold
// for safety (avoid promoting marginal slots).
const unsigned kAccessThreshold = 5u;
const unsigned kMaxPromote = 2u;
DenseMap<int, unsigned> FiToImgIdx;
unsigned NextFreeImg = 0;
for (int FI : Ordered) {
if (AccessCount[FI] < kAccessThreshold) break;
if (FiToImgIdx.size() >= kMaxPromote) break;
while (NextFreeImg < 8 && UsedImg.test(NextFreeImg)) ++NextFreeImg;
if (NextFreeImg >= 8) break;
FiToImgIdx[FI] = NextFreeImg; // Map to IMG0..7 (caller-save)
++NextFreeImg;
}
if (FiToImgIdx.empty()) return false;
// 5. Rewrite each access. Insert the new DP MC inst before the
// pseudo, then erase the pseudo. Preserve flags and tied-def
// semantics via implicit operands.
bool Changed = false;
for (auto &P : FiToImgIdx) {
int FI = P.first;
unsigned ImgIdx = P.second;
uint8_t DpAddr = dpAddrForImg(ImgIdx);
LLVM_DEBUG(dbgs() << "Promote fi#" << FI << " -> IMG"
<< ImgIdx << " ($" << format("%02x", DpAddr)
<< "), " << AccessCount[FI] << " accesses\n");
for (MachineInstr *MI : AccessSites[FI]) {
unsigned Opc = MI->getOpcode();
unsigned NewOpc = getDpOpcode(Opc);
if (!NewOpc) continue;
MachineBasicBlock *MBB = MI->getParent();
DebugLoc DL = MI->getDebugLoc();
MachineInstrBuilder NewMI =
BuildMI(*MBB, MI, DL, TII->get(NewOpc)).addImm(DpAddr);
// Carry implicit-def $a (LDA/ADC/SBC/AND/ORA/EOR all write $a)
// and implicit-use $a (STA/CMP/ADC/SBC/AND/ORA/EOR all read $a).
// ADCfi/SBCfi additionally use $p; their DP equivalents read $p
// implicitly via the tablegen Defs/Uses. But since we built the
// new MI from TII->get(NewOpc), the implicit operands from the
// descriptor are auto-added. We only need to copy non-FI explicit
// operands... which for our pseudos are register operands. The
// physical register defs/uses they carry must be preserved.
for (const MachineOperand &MO : MI->operands()) {
if (MO.isReg() && MO.getReg().isPhysical() && MO.isImplicit()) {
// Skip — already added by descriptor.
continue;
}
if (MO.isReg() && MO.getReg().isPhysical() && !MO.isImplicit()) {
// Explicit physreg operand (e.g., the $a in STAfi $a, fi, 0).
// Convert to implicit so the DP MC inst's descriptor matches.
RegState Flags = MO.isDef() ? RegState::ImplicitDefine
: RegState::Implicit;
if (MO.isKill()) Flags = Flags | RegState::Kill;
NewMI.addReg(MO.getReg(), Flags);
}
// FI/offset operands are skipped — replaced by the DP imm above.
// VReg defs/uses should be gone post-RA; if any survived, skip.
}
MI->eraseFromParent();
Changed = true;
}
// Mark the FI as dead so PEI can skip allocating stack for it.
// MFI doesn't expose RemoveStackObject publicly, but setting size
// to 0 also works in most code paths. Actually leave it alive —
// a 2-byte unused slot is cheap, and removing exposes us to
// PEI bugs.
}
return Changed;
}