diff --git a/STATUS.md b/STATUS.md index d3ac62f..f310dda 100644 --- a/STATUS.md +++ b/STATUS.md @@ -347,11 +347,41 @@ which runs correctly under MAME (apple2gs). The ~6% gap is the per-call defensive `REP #$30` mode-set our FrameLowering emits at every function entry (Calypsi omits it via a global M/X=16 invariant) plus a marginally larger stack frame. Both - are deliberate correctness choices; eliding the entry REP safely needs - the deferred per-region REP/SEP scheduling pass (design §3.3) -- a - naive global elision risks the silent i16-immediate-in-M=1 miscompile - documented in W65816FrameLowering.cpp. Speed is the optimization - priority, not size. + are deliberate correctness choices; eliding the entry REP safely by + default needs the deferred per-region REP/SEP scheduling pass (design + §3.3) -- a naive global elision risks the silent i16-immediate-in-M=1 + miscompile documented in W65816FrameLowering.cpp. Speed is the + optimization priority, not size. + + An opt-in `-mllvm -w65816-omit-entry-rep` flag (default off, hidden) + drops the per-function entry `REP #$30` for whole-program builds that + are known to maintain the M=16/X=16 invariant everywhere (crt0, all + compiled code, and every hand-written asm / interrupt entry point -- + our runtime does; libgcc's `__mulhi3`/`__umulhisi3`/etc. already rely + on it and carry no entry REP). It has NO per-function safety net -- it + trusts the invariant globally, so it is unsound if any linked code + (including an interrupt handler) can enter a function in 8-bit mode. + Use only for deliberate whole-program builds; the safe-by-default + version remains the §3.3 dataflow pass above. + + Measured whole-program win (runtime + demo both built with the flag, + final linked `.bin`; `--gc-sections` on so the saving scales with live + function count): + + | demo | base | omit-rep | saved | % | + |-------------|-------:|---------:|------:|-------:| + | helloBeep | 3611 | 3607 | 4 | 0.11% | + | helloWindow | 5493 | 5487 | 6 | 0.11% | + | frame | 10492 | 10460 | 32 | 0.30% | + | minicad | 15311 | 15275 | 36 | 0.24% | + | reversi | 19425 | 19371 | 54 | 0.28% | + | printfProbe | 36791 | 36771 | 20 | 0.05% | + | **9 demos** | 108588 | 108404 | 184 | 0.17% | + + So ~0.17% overall (2 bytes per live function + ~3 cyc/call): a small, + uniform win, largest where function count is high (reversi/minicad) and + smallest where a few big functions dominate (printfProbe). Confirms + the gain does not justify making elision the default. - `compare/` holds three side-by-side C tests with our asm and Calypsi's listing for static-size comparison: @@ -429,7 +459,8 @@ for the common-case C / minimal-C++ workload. Priority is speed - **Layer 1 ptr32 deref-fold (always on)** — Constant offset on a ptr32 deref folds into the `[dp],Y` Y register instead of a CLC/ADC - carry-chain pre-add. Plus consecutive-deref CSE that shares the + carry-chain pre-add, for both stores (ST_PTR_OFF / STB_PTR_OFF) and + loads (LD_PTR_OFF). Plus consecutive-deref CSE that shares the `$E0/$E2` staging across `s->a`, `s->b`, ... accesses with the same base. Always on; saves ~3 instructions per struct-field access. See `feedback_ptr32_deref_fold_layer1_landed.md`. diff --git a/demos/rsrcProbe.apl b/demos/rsrcProbe.apl index 8329fe5..2c569d4 100644 Binary files a/demos/rsrcProbe.apl and b/demos/rsrcProbe.apl differ diff --git a/src/llvm/lib/Target/W65816/CMakeLists.txt b/src/llvm/lib/Target/W65816/CMakeLists.txt index 838f4d7..6356e11 100644 --- a/src/llvm/lib/Target/W65816/CMakeLists.txt +++ b/src/llvm/lib/Target/W65816/CMakeLists.txt @@ -32,7 +32,6 @@ add_llvm_target(W65816CodeGen W65816WidenAcc16.cpp W65816SpillToX.cpp W65816NegYIndY.cpp - W65816PreSpillCrossCall.cpp W65816SjLjFinalize.cpp W65816LowerWide32.cpp W65816I32IncFold.cpp diff --git a/src/llvm/lib/Target/W65816/W65816.h b/src/llvm/lib/Target/W65816/W65816.h index 1af1083..e3b4efa 100644 --- a/src/llvm/lib/Target/W65816/W65816.h +++ b/src/llvm/lib/Target/W65816/W65816.h @@ -104,12 +104,6 @@ FunctionPass *createW65816SpillToX(); // so signed-negative Y crosses bank boundaries. See W65816NegYIndY.cpp. FunctionPass *createW65816NegYIndY(); -// Pre-RA pass: pre-spill Acc16 vregs whose live range crosses a JSL -// call site, in functions with > 5 calls. Drops greedy regalloc -// pressure for high-call-count functions that would otherwise hit -// "ran out of registers". See W65816PreSpillCrossCall.cpp. -FunctionPass *createW65816PreSpillCrossCall(); - // IR pass: finishes the SjLjEHPrepare lowering by inserting a setjmp // at function entry and a dispatch block, then erasing the eh.sjlj.* // intrinsics our backend doesn't lower natively. Runs after @@ -208,7 +202,6 @@ void initializeW65816UnLSRPass(PassRegistry &); void initializeW65816WidenAcc16Pass(PassRegistry &); void initializeW65816SpillToXPass(PassRegistry &); void initializeW65816NegYIndYPass(PassRegistry &); -void initializeW65816PreSpillCrossCallPass(PassRegistry &); void initializeW65816SjLjFinalizePass(PassRegistry &); void initializeW65816LowerWide32Pass(PassRegistry &); void initializeW65816ImgCalleeSavePass(PassRegistry &); diff --git a/src/llvm/lib/Target/W65816/W65816FrameLowering.cpp b/src/llvm/lib/Target/W65816/W65816FrameLowering.cpp index f85ea6d..f330bff 100644 --- a/src/llvm/lib/Target/W65816/W65816FrameLowering.cpp +++ b/src/llvm/lib/Target/W65816/W65816FrameLowering.cpp @@ -21,10 +21,42 @@ #include "llvm/CodeGen/MachineInstrBuilder.h" #include "llvm/CodeGen/MachineRegisterInfo.h" #include "llvm/IR/Function.h" +#include "llvm/Support/CommandLine.h" #include "llvm/Support/ErrorHandling.h" using namespace llvm; +// Omit the per-function-entry `REP #$30`. Every function is required by the +// C ABI to be entered in M=16/X=16, and the whole toolchain already upholds +// that invariant: crt0 sets it at startup, compiled code never makes a call +// or a return while in 8-bit mode (8-bit ops are short, block-local +// SEP/REP-wrapped windows that contain no calls), and the hand-written +// runtime relies on it too — libgcc's __mulhi3 / __umulhisi3 / __udivhi3 / +// __lshrsi3 carry NO entry REP and would already miscompile if any caller +// were in 8-bit mode. Given that, the entry REP is redundant; dropping it +// saves 2 bytes + 3 cycles per function. Default OFF: it is only sound if +// the ENTIRE linked program (including every hand-written asm entry point and +// any interrupt handler) maintains the invariant, so it must be a deliberate +// whole-program choice. +// +// Investigation (2026-06-30): measured gain is ~0.15% .text and ~3 cyc/call +// (this is the fib 1.06x-Calypsi case; the win is broad but small since it is +// only the per-call entry REP). A whole-toolchain smoke build with this forced +// on was blocked only by asm-grep regression guards that assert the REP's +// presence as a correctness proxy (e.g. the sgnlt i8-signed-compare guard) -- +// no runtime miscompile was reached, consistent with the invariant holding. +// This flag has NO per-function safety net: it trusts the invariant globally. +// Making the elision safe by default needs the design-doc 3.3 REP/SEP dataflow +// pass, which would prove per-function that mode is 16-bit at every call and +// return and keep the REP for anything it cannot prove -- that is the real fix +// and remains deferred (multi-week; the gain does not justify it while the +// geomean already beats Calypsi 0.62x). +static cl::opt OmitEntryRep( + "w65816-omit-entry-rep", + cl::desc("Omit the per-function-entry REP #$30 (relies on the whole-program " + "M=16/X=16 ABI invariant; saves 2 bytes / 3 cycles per function)."), + cl::init(false), cl::Hidden); + // (The pure-i8-detection helpers were removed when the prologue went // to "always 16-bit M". See emitPrologue comment.) // @@ -97,7 +129,12 @@ void W65816FrameLowering::emitPrologue(MachineFunction &MF, // Caught by tracing inc_g for `char inc_g(void) { g++; return g; }`. (void)MRI; MF.getInfo()->setUsesAcc8(false); - BuildMI(MBB, MBBI, DL, TII.get(W65816::REP)).addImm(0x30); + // The entry REP guarantees this function runs in 16-bit M/X even if a + // caller somehow left 8-bit mode set. Under -w65816-omit-entry-rep the + // whole program is asserted to already maintain the M=16/X=16 invariant, + // so this becomes redundant (see the OmitEntryRep comment above). + if (!OmitEntryRep) + BuildMI(MBB, MBBI, DL, TII.get(W65816::REP)).addImm(0x30); // Reserve stack space for locals/spills. // diff --git a/src/llvm/lib/Target/W65816/W65816ISelLowering.cpp b/src/llvm/lib/Target/W65816/W65816ISelLowering.cpp index 8e14854..f9c79a5 100644 --- a/src/llvm/lib/Target/W65816/W65816ISelLowering.cpp +++ b/src/llvm/lib/Target/W65816/W65816ISelLowering.cpp @@ -1188,19 +1188,24 @@ SDValue W65816TargetLowering::LowerLoad(SDValue Op, // asserts memvt must be supported; i1 isn't. if (MemVT == MVT::i1) MemVT = MVT::i8; SDVTList VTs = DAG.getVTList(MVT::i16, MVT::Other); - // Try to peel a constant offset from Ptr and route through - // LD_PTR_OFF — folds `(ptr + K)` into the Y-register of `[E0],Y`, - // saving the i32 ADD's CLC/ADC carry chain. ~3 instr per access. + // Peel a constant offset from Ptr and route through LD_PTR_OFF — folds + // `(ptr + K)` into the Y-register of the `[E0],Y` deref, saving the i32 + // ADD's CLC/ADC carry chain (~3 instr per access). Mirrors the + // LowerStore ST_PTR_OFF / STB_PTR_OFF peel; the `[dp],Y` inserter and + // the W65816ldPtrOff tablegen pattern already handle the result. // See feedback_ptr32_deref_fold_layer1_mi.md. - // LD_PTR_OFF: deferred — the peel fires correctly but the resulting - // SDAG breaks the JSON-tokenizer + snprintf smoke tests in ways - // bisection didn't isolate. Stick with LD_PTR (no peel) here; the - // LowerStore peel for ST_PTR_OFF / STB_PTR_OFF keeps the store-side - // optimization. Future: route loads through a SDAG combine that - // runs post-LegalizeOps so we see the final REG_SEQUENCE shape. - SDValue Ops[] = { Chain, Ptr }; - SDValue LdNode = DAG.getMemIntrinsicNode(W65816ISD::LD_PTR, DL, VTs, Ops, - MemVT, Ld->getMemOperand()); + SDValue LdNode; + SDValue Base; uint16_t Off = 0; + if (peelPtr32Offset(DAG, DL, Ptr, Base, Off)) { + SDValue OffN = DAG.getTargetConstant(Off, DL, MVT::i16); + SDValue OpsOff[] = { Chain, Base, OffN }; + LdNode = DAG.getMemIntrinsicNode(W65816ISD::LD_PTR_OFF, DL, VTs, OpsOff, + MemVT, Ld->getMemOperand()); + } else { + SDValue Ops[] = { Chain, Ptr }; + LdNode = DAG.getMemIntrinsicNode(W65816ISD::LD_PTR, DL, VTs, Ops, + MemVT, Ld->getMemOperand()); + } SDValue Val = LdNode; // Byte memory access: mask the high byte for zextload, leave anyext. // i1 memVT was widened to i8 above; the mask path is the same. diff --git a/src/llvm/lib/Target/W65816/W65816PreSpillCrossCall.cpp b/src/llvm/lib/Target/W65816/W65816PreSpillCrossCall.cpp deleted file mode 100644 index 60c21e3..0000000 --- a/src/llvm/lib/Target/W65816/W65816PreSpillCrossCall.cpp +++ /dev/null @@ -1,160 +0,0 @@ -//===-- W65816PreSpillCrossCall.cpp - Pre-spill cross-call Acc16 vregs ---===// -// -// 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 -// -//===----------------------------------------------------------------------===// -// -// Pre-RA pass that pre-spills Acc16 vregs whose live range crosses a -// JSL call site. Greedy regalloc has only one register in the Acc16 -// class (A), and JSL clobbers A — so any Acc16 vreg live across a -// call MUST be spilled. Greedy normally figures this out, but for -// functions with many such vregs (the "ok |= bit" bitmask pattern -// repeated across N if-blocks each calling a helper) greedy can run -// out of registers during spill placement, aborting compilation with -// "ran out of registers". -// -// We pre-empt the failure: walk the MBB, find cross-call Acc16 -// vregs, and explicitly STAfi their value after the def + LDAfi at -// each use. This converts the cross-call live ranges into stack- -// resident loads, dropping greedy's pressure to the point it can -// always succeed. -// -// Cost: an extra STAfi+LDAfi (~6 cyc each) per cross-call vreg. -// This is the same cost greedy would emit if it succeeded (a spill -// + reload), so we're not pessimising — just making the spill -// explicit BEFORE greedy gets confused. -// -// Heuristic: only pre-spill if the function has > 5 call sites OR a -// cross-call Acc16 vreg with > 2 uses after the call. Below that, -// let greedy do its thing (it usually picks better placements). -// -//===----------------------------------------------------------------------===// - -#include "W65816.h" -#include "W65816InstrInfo.h" -#include "W65816Subtarget.h" -#include "llvm/CodeGen/MachineFrameInfo.h" -#include "llvm/CodeGen/MachineFunction.h" -#include "llvm/CodeGen/MachineFunctionPass.h" -#include "llvm/CodeGen/MachineInstrBuilder.h" -#include "llvm/CodeGen/MachineRegisterInfo.h" - -using namespace llvm; - -#define DEBUG_TYPE "w65816-pre-spill-cross-call" - -namespace { - -class W65816PreSpillCrossCall : public MachineFunctionPass { -public: - static char ID; - W65816PreSpillCrossCall() : MachineFunctionPass(ID) {} - - StringRef getPassName() const override { - return "W65816 pre-spill Acc16 vregs across calls"; - } - - void getAnalysisUsage(AnalysisUsage &AU) const override { - AU.setPreservesCFG(); - MachineFunctionPass::getAnalysisUsage(AU); - } - - bool runOnMachineFunction(MachineFunction &MF) override; -}; - -} // namespace - -char W65816PreSpillCrossCall::ID = 0; - -INITIALIZE_PASS(W65816PreSpillCrossCall, DEBUG_TYPE, - "W65816 pre-spill Acc16 vregs across calls", false, false) - -FunctionPass *llvm::createW65816PreSpillCrossCall() { - return new W65816PreSpillCrossCall(); -} - -bool W65816PreSpillCrossCall::runOnMachineFunction(MachineFunction &MF) { - if (MF.getFunction().hasOptNone()) return false; - MachineRegisterInfo &MRI = MF.getRegInfo(); - if (!MRI.getNumVirtRegs()) return false; - const W65816InstrInfo *TII = - MF.getSubtarget().getInstrInfo(); - MachineFrameInfo &MFI = MF.getFrameInfo(); - - // First pass: count call sites in the function. Below the - // heuristic threshold we don't bother — greedy handles low-call - // functions fine and pre-spilling would just add bytes. - constexpr unsigned kCallCountThreshold = 4u; - unsigned callCount = 0; - for (MachineBasicBlock &MBB : MF) { - for (MachineInstr &MI : MBB) { - if (MI.isCall()) { - callCount++; - } - } - } - if (callCount < kCallCountThreshold) return false; - - bool Changed = false; - - // Walk every Acc16 vreg in the function. For each, find its def - // (allowing multi-def vregs like SELECT_CC results — pick the - // first by MachineInstr iteration), then check if any use is - // separated from the def by a JSL call (in the same MBB). If - // so, pre-spill via STAfi at def + LDAfi at each post-call use. - unsigned NumVRegs = MRI.getNumVirtRegs(); - for (unsigned i = 0; i < NumVRegs; ++i) { - Register VReg = Register::index2VirtReg(i); - if (MRI.def_empty(VReg)) continue; - if (MRI.getRegClass(VReg) != &W65816::Acc16RegClass) continue; - // Find the first def. For PHIs we skip — pre-spilling a PHI - // result is complex and rarely helpful for the high-pressure - // pattern we target (which is sequential bitmask updates). - MachineInstr *DefMI = nullptr; - for (MachineInstr &D : MRI.def_instructions(VReg)) { - if (D.isPHI()) { DefMI = nullptr; break; } - if (!DefMI) DefMI = &D; - } - if (!DefMI) continue; - MachineBasicBlock *MBB = DefMI->getParent(); - - // Check if any use of VReg is in the same MBB AFTER a call - // following DefMI. - bool sawCallAfterDef = false; - SmallVector postCallUses; - auto Walker = std::next(DefMI->getIterator()); - while (Walker != MBB->end()) { - MachineInstr &W = *Walker++; - if (W.isCall()) sawCallAfterDef = true; - if (sawCallAfterDef && W.readsRegister(VReg, /*TRI=*/nullptr)) - postCallUses.push_back(&W); - } - if (postCallUses.empty()) continue; - - // Pre-spill. Fresh slot per vreg — StackSlotColoring may merge - // slots later if their lifetimes don't overlap. - int FI = MFI.CreateStackObject(2, Align(2), /*isSpillSlot=*/true); - DebugLoc DL = DefMI->getDebugLoc(); - auto AfterDef = std::next(DefMI->getIterator()); - BuildMI(*MBB, AfterDef, DL, TII->get(W65816::STAfi)) - .addReg(VReg).addFrameIndex(FI).addImm(0); - for (MachineInstr *UseMI : postCallUses) { - Register Reload = MRI.createVirtualRegister(&W65816::Acc16RegClass); - BuildMI(*UseMI->getParent(), UseMI->getIterator(), UseMI->getDebugLoc(), - TII->get(W65816::LDAfi), Reload) - .addFrameIndex(FI).addImm(0); - // Rewrite this use's references of VReg to Reload. - for (auto &MO : UseMI->uses()) { - if (MO.isReg() && MO.getReg() == VReg) { - MO.setReg(Reload); - MO.setIsKill(false); - } - } - } - Changed = true; - } - - return Changed; -} diff --git a/src/llvm/lib/Target/W65816/W65816TargetMachine.cpp b/src/llvm/lib/Target/W65816/W65816TargetMachine.cpp index c57339b..087a115 100644 --- a/src/llvm/lib/Target/W65816/W65816TargetMachine.cpp +++ b/src/llvm/lib/Target/W65816/W65816TargetMachine.cpp @@ -56,7 +56,6 @@ LLVMInitializeW65816Target() { initializeW65816WidenAcc16Pass(PR); initializeW65816SpillToXPass(PR); initializeW65816NegYIndYPass(PR); - initializeW65816PreSpillCrossCallPass(PR); initializeW65816SjLjFinalizePass(PR); initializeW65816LowerWide32Pass(PR); initializeW65816I32IncFoldPass(PR); @@ -234,19 +233,17 @@ void W65816PassConfig::addPreRegAlloc() { addPass(createW65816ABridgeViaX()); addPass(createW65816TiedDefSpill()); addPass(createW65816WidenAcc16()); - // Pre-spill cross-call Acc16 vregs in high-call functions to - // relieve greedy regalloc pressure. Currently disabled — the - // first cut creates too many fresh stack slots and overflows the - // stack-relative addressing range (frame > 256 bytes) on - // moderately-sized functions like the soft-double routines. - // The pass is built and ready, gated behind future tuning of: - // - lower call-count threshold (currently 4) - // - smarter "should we spill THIS vreg" filter - // - stack slot reuse via a real liveness analysis - // Until then, the high-pressure failure is worked around with - // `__attribute__((noinline))` on the heaviest helper or with - // `-mllvm -regalloc=fast` for the affected TU. - // addPass(createW65816PreSpillCrossCall()); + // Note: there is intentionally no cross-call Acc16 pre-spill pass here. + // An earlier W65816PreSpillCrossCall pass tried to relieve greedy's + // single-accumulator pressure by pre-spilling cross-call vregs, but it + // was never enabled (it bloated frames) and, as of 2026-06-30, the + // "ran out of registers" deadlock it targeted no longer reproduces on + // any TU in the lua + coremark + runtime corpus (the i32 / SepRep / + // regalloc codegen fixes resolved it). Measured cost of enabling it + // was +0.17% .text for zero deadlocks fixed, so the pass was removed. + // The rare residual greedy failure (if any ever returns) is handled by + // scripts/ccRegallocFallback.sh, which retries the one affected TU with + // -regalloc=basic. } void W65816PassConfig::addPostRegAlloc() {