More deferred issues resolved.

This commit is contained in:
Scott Duensing 2026-06-30 20:34:32 -05:00
parent 1a0ef7ee87
commit d10d3b9fc6
8 changed files with 103 additions and 201 deletions

View file

@ -347,11 +347,41 @@ which runs correctly under MAME (apple2gs).
The ~6% gap is the per-call defensive `REP #$30` mode-set our The ~6% gap is the per-call defensive `REP #$30` mode-set our
FrameLowering emits at every function entry (Calypsi omits it via a FrameLowering emits at every function entry (Calypsi omits it via a
global M/X=16 invariant) plus a marginally larger stack frame. Both global M/X=16 invariant) plus a marginally larger stack frame. Both
are deliberate correctness choices; eliding the entry REP safely needs are deliberate correctness choices; eliding the entry REP safely by
the deferred per-region REP/SEP scheduling pass (design §3.3) -- a default needs the deferred per-region REP/SEP scheduling pass (design
naive global elision risks the silent i16-immediate-in-M=1 miscompile §3.3) -- a naive global elision risks the silent i16-immediate-in-M=1
documented in W65816FrameLowering.cpp. Speed is the optimization miscompile documented in W65816FrameLowering.cpp. Speed is the
priority, not size. 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 - `compare/` holds three side-by-side C tests with our asm and
Calypsi's listing for static-size comparison: 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 - **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 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 `$E0/$E2` staging across `s->a`, `s->b`, ... accesses with the same
base. Always on; saves ~3 instructions per struct-field access. base. Always on; saves ~3 instructions per struct-field access.
See `feedback_ptr32_deref_fold_layer1_landed.md`. See `feedback_ptr32_deref_fold_layer1_landed.md`.

Binary file not shown.

View file

@ -32,7 +32,6 @@ add_llvm_target(W65816CodeGen
W65816WidenAcc16.cpp W65816WidenAcc16.cpp
W65816SpillToX.cpp W65816SpillToX.cpp
W65816NegYIndY.cpp W65816NegYIndY.cpp
W65816PreSpillCrossCall.cpp
W65816SjLjFinalize.cpp W65816SjLjFinalize.cpp
W65816LowerWide32.cpp W65816LowerWide32.cpp
W65816I32IncFold.cpp W65816I32IncFold.cpp

View file

@ -104,12 +104,6 @@ FunctionPass *createW65816SpillToX();
// so signed-negative Y crosses bank boundaries. See W65816NegYIndY.cpp. // so signed-negative Y crosses bank boundaries. See W65816NegYIndY.cpp.
FunctionPass *createW65816NegYIndY(); 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 // IR pass: finishes the SjLjEHPrepare lowering by inserting a setjmp
// at function entry and a dispatch block, then erasing the eh.sjlj.* // at function entry and a dispatch block, then erasing the eh.sjlj.*
// intrinsics our backend doesn't lower natively. Runs after // intrinsics our backend doesn't lower natively. Runs after
@ -208,7 +202,6 @@ void initializeW65816UnLSRPass(PassRegistry &);
void initializeW65816WidenAcc16Pass(PassRegistry &); void initializeW65816WidenAcc16Pass(PassRegistry &);
void initializeW65816SpillToXPass(PassRegistry &); void initializeW65816SpillToXPass(PassRegistry &);
void initializeW65816NegYIndYPass(PassRegistry &); void initializeW65816NegYIndYPass(PassRegistry &);
void initializeW65816PreSpillCrossCallPass(PassRegistry &);
void initializeW65816SjLjFinalizePass(PassRegistry &); void initializeW65816SjLjFinalizePass(PassRegistry &);
void initializeW65816LowerWide32Pass(PassRegistry &); void initializeW65816LowerWide32Pass(PassRegistry &);
void initializeW65816ImgCalleeSavePass(PassRegistry &); void initializeW65816ImgCalleeSavePass(PassRegistry &);

View file

@ -21,10 +21,42 @@
#include "llvm/CodeGen/MachineInstrBuilder.h" #include "llvm/CodeGen/MachineInstrBuilder.h"
#include "llvm/CodeGen/MachineRegisterInfo.h" #include "llvm/CodeGen/MachineRegisterInfo.h"
#include "llvm/IR/Function.h" #include "llvm/IR/Function.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/Support/ErrorHandling.h" #include "llvm/Support/ErrorHandling.h"
using namespace llvm; 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<bool> 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 // (The pure-i8-detection helpers were removed when the prologue went
// to "always 16-bit M". See emitPrologue comment.) // to "always 16-bit M". See emitPrologue comment.)
// //
@ -97,6 +129,11 @@ void W65816FrameLowering::emitPrologue(MachineFunction &MF,
// Caught by tracing inc_g for `char inc_g(void) { g++; return g; }`. // Caught by tracing inc_g for `char inc_g(void) { g++; return g; }`.
(void)MRI; (void)MRI;
MF.getInfo<W65816MachineFunctionInfo>()->setUsesAcc8(false); MF.getInfo<W65816MachineFunctionInfo>()->setUsesAcc8(false);
// 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); BuildMI(MBB, MBBI, DL, TII.get(W65816::REP)).addImm(0x30);
// Reserve stack space for locals/spills. // Reserve stack space for locals/spills.

View file

@ -1188,19 +1188,24 @@ SDValue W65816TargetLowering::LowerLoad(SDValue Op,
// asserts memvt must be supported; i1 isn't. // asserts memvt must be supported; i1 isn't.
if (MemVT == MVT::i1) MemVT = MVT::i8; if (MemVT == MVT::i1) MemVT = MVT::i8;
SDVTList VTs = DAG.getVTList(MVT::i16, MVT::Other); SDVTList VTs = DAG.getVTList(MVT::i16, MVT::Other);
// Try to peel a constant offset from Ptr and route through // Peel a constant offset from Ptr and route through LD_PTR_OFF — folds
// LD_PTR_OFF — folds `(ptr + K)` into the Y-register of `[E0],Y`, // `(ptr + K)` into the Y-register of the `[E0],Y` deref, saving the i32
// saving the i32 ADD's CLC/ADC carry chain. ~3 instr per access. // 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. // See feedback_ptr32_deref_fold_layer1_mi.md.
// LD_PTR_OFF: deferred — the peel fires correctly but the resulting SDValue LdNode;
// SDAG breaks the JSON-tokenizer + snprintf smoke tests in ways SDValue Base; uint16_t Off = 0;
// bisection didn't isolate. Stick with LD_PTR (no peel) here; the if (peelPtr32Offset(DAG, DL, Ptr, Base, Off)) {
// LowerStore peel for ST_PTR_OFF / STB_PTR_OFF keeps the store-side SDValue OffN = DAG.getTargetConstant(Off, DL, MVT::i16);
// optimization. Future: route loads through a SDAG combine that SDValue OpsOff[] = { Chain, Base, OffN };
// runs post-LegalizeOps so we see the final REG_SEQUENCE shape. LdNode = DAG.getMemIntrinsicNode(W65816ISD::LD_PTR_OFF, DL, VTs, OpsOff,
SDValue Ops[] = { Chain, Ptr };
SDValue LdNode = DAG.getMemIntrinsicNode(W65816ISD::LD_PTR, DL, VTs, Ops,
MemVT, Ld->getMemOperand()); MemVT, Ld->getMemOperand());
} else {
SDValue Ops[] = { Chain, Ptr };
LdNode = DAG.getMemIntrinsicNode(W65816ISD::LD_PTR, DL, VTs, Ops,
MemVT, Ld->getMemOperand());
}
SDValue Val = LdNode; SDValue Val = LdNode;
// Byte memory access: mask the high byte for zextload, leave anyext. // Byte memory access: mask the high byte for zextload, leave anyext.
// i1 memVT was widened to i8 above; the mask path is the same. // i1 memVT was widened to i8 above; the mask path is the same.

View file

@ -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<W65816Subtarget>().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<MachineInstr *, 4> 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;
}

View file

@ -56,7 +56,6 @@ LLVMInitializeW65816Target() {
initializeW65816WidenAcc16Pass(PR); initializeW65816WidenAcc16Pass(PR);
initializeW65816SpillToXPass(PR); initializeW65816SpillToXPass(PR);
initializeW65816NegYIndYPass(PR); initializeW65816NegYIndYPass(PR);
initializeW65816PreSpillCrossCallPass(PR);
initializeW65816SjLjFinalizePass(PR); initializeW65816SjLjFinalizePass(PR);
initializeW65816LowerWide32Pass(PR); initializeW65816LowerWide32Pass(PR);
initializeW65816I32IncFoldPass(PR); initializeW65816I32IncFoldPass(PR);
@ -234,19 +233,17 @@ void W65816PassConfig::addPreRegAlloc() {
addPass(createW65816ABridgeViaX()); addPass(createW65816ABridgeViaX());
addPass(createW65816TiedDefSpill()); addPass(createW65816TiedDefSpill());
addPass(createW65816WidenAcc16()); addPass(createW65816WidenAcc16());
// Pre-spill cross-call Acc16 vregs in high-call functions to // Note: there is intentionally no cross-call Acc16 pre-spill pass here.
// relieve greedy regalloc pressure. Currently disabled — the // An earlier W65816PreSpillCrossCall pass tried to relieve greedy's
// first cut creates too many fresh stack slots and overflows the // single-accumulator pressure by pre-spilling cross-call vregs, but it
// stack-relative addressing range (frame > 256 bytes) on // was never enabled (it bloated frames) and, as of 2026-06-30, the
// moderately-sized functions like the soft-double routines. // "ran out of registers" deadlock it targeted no longer reproduces on
// The pass is built and ready, gated behind future tuning of: // any TU in the lua + coremark + runtime corpus (the i32 / SepRep /
// - lower call-count threshold (currently 4) // regalloc codegen fixes resolved it). Measured cost of enabling it
// - smarter "should we spill THIS vreg" filter // was +0.17% .text for zero deadlocks fixed, so the pass was removed.
// - stack slot reuse via a real liveness analysis // The rare residual greedy failure (if any ever returns) is handled by
// Until then, the high-pressure failure is worked around with // scripts/ccRegallocFallback.sh, which retries the one affected TU with
// `__attribute__((noinline))` on the heaviest helper or with // -regalloc=basic.
// `-mllvm -regalloc=fast` for the affected TU.
// addPass(createW65816PreSpillCrossCall());
} }
void W65816PassConfig::addPostRegAlloc() { void W65816PassConfig::addPostRegAlloc() {