//===-- 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(); 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(); 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().getLI(); DenseMap AccessCount; DenseMap> 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 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 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; }