mirror of
https://github.com/imjasonh/nescript
synced 2026-07-08 08:55:38 +00:00
peephole: drop dead LDA #imm before mem-INC/DEC + JMP
The IR codegen lowers `i -= 1` (and friends) into a `LoadImm temp,
1; Sub d, i, temp; StoreVar i, d` triple, and the optimizer
strength-reduces the Sub+StoreVar pair into `DEC i`. The
constant-load-into-A that used to feed the Sub stays around as a
dead `LDA #1`:
LDA #1
DEC ZeroPage(rem)
JMP Label("__ir_blk_while_cond_…")
`remove_dead_loads` was set up to drop exactly this pattern but
gave up at the trailing `JMP` because it couldn't reason about
flow. Extend it to follow one unconditional `JMP <label>` to its
target and resume the dead-store scan from the next instruction.
The first instruction past the loop-condition label is reliably an
`LDA loop_var`, which overwrites A without reading it — so the
`LDA #1` is correctly identified as dead.
Conditional branches still end the scan (their not-taken path is
unconstrained) and only one JMP is followed (to keep the analysis
local). For SHA-256 specifically this drops two `LDA #1`s per
iteration of the rotate/shift bit-loops — about 1K cycles per
block. The same pattern fires across most examples' loop tails.
Verified: cargo test/clippy/fmt clean on rustc 1.95.0; emulator
harness 34/34; reproducibility diff clean; SHA-256 of "NES" still
computes to AE9145DB…4E0D. The cycle drift refreshes the four
audio hashes / golden frames timing-sensitive examples already
tracked.
https://claude.ai/code/session_01FRmSBruVWCufm3LsUVMs8v
This commit is contained in:
parent
0600f5b872
commit
df71c2bf50
12 changed files with 40 additions and 22 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
examples/war.nes
BIN
examples/war.nes
Binary file not shown.
|
|
@ -231,11 +231,22 @@ fn invert_branch(op: Opcode) -> Option<Opcode> {
|
||||||
/// Remove `LDA …` instructions whose value is never read — the next
|
/// Remove `LDA …` instructions whose value is never read — the next
|
||||||
/// instruction overwrites A without using the current value.
|
/// instruction overwrites A without using the current value.
|
||||||
///
|
///
|
||||||
/// The heuristic looks one instruction ahead. If the next instruction
|
/// The heuristic looks forward, stepping over instructions that
|
||||||
/// is an A-clobbering load (`LDA`, `LDX`, `LDY`, `PLA`, `TXA`, `TYA`),
|
/// don't touch A (memory `INC`/`DEC`/`STX`/`STY`, labels). If the
|
||||||
/// the preceding `LDA` is dead. Shifts and arithmetic ops read A, so
|
/// first instruction that *does* touch A overwrites it without
|
||||||
/// they don't qualify. A label or branch in between stops the scan
|
/// reading it (`LDA`, `PLA`, `TXA`, `TYA`), the preceding `LDA` is
|
||||||
/// (we can't prove A's dead across control flow).
|
/// dead. Shifts and arithmetic ops read A, so they end the scan
|
||||||
|
/// without marking dead.
|
||||||
|
///
|
||||||
|
/// One unconditional `JMP` is followed: we look up its target label
|
||||||
|
/// and resume scanning from the first instruction after it. This
|
||||||
|
/// catches `LDA #imm; DEC zp; JMP loop_cond; loop_cond: LDA loop_var`
|
||||||
|
/// patterns that the IR codegen leaves behind for `i -= 1`-style
|
||||||
|
/// loops, where the `LDA #1` was the constant operand of a `Sub`
|
||||||
|
/// the optimizer already strength-reduced into the `DEC`. Conditional
|
||||||
|
/// branches and `JSR` still end the scan — JSR could land on a
|
||||||
|
/// runtime helper that reads A, and a branch's not-taken path is
|
||||||
|
/// unconstrained.
|
||||||
fn remove_dead_loads(instructions: &mut Vec<Instruction>) {
|
fn remove_dead_loads(instructions: &mut Vec<Instruction>) {
|
||||||
let mut keep = vec![true; instructions.len()];
|
let mut keep = vec![true; instructions.len()];
|
||||||
for i in 0..instructions.len() {
|
for i in 0..instructions.len() {
|
||||||
|
|
@ -243,19 +254,9 @@ fn remove_dead_loads(instructions: &mut Vec<Instruction>) {
|
||||||
if inst.opcode != Opcode::LDA {
|
if inst.opcode != Opcode::LDA {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Walk forward looking for the next instruction that either
|
|
||||||
// reads A or overwrites it. If the first such instruction
|
|
||||||
// overwrites A without reading it, our LDA is dead.
|
|
||||||
//
|
|
||||||
// We can safely step across instructions that neither read
|
|
||||||
// nor write A — INC, DEC, STX, STY on memory, and label
|
|
||||||
// definitions — because they don't observe A and don't
|
|
||||||
// clobber it either. We must NOT step over any branch or
|
|
||||||
// jump: control flow at that point can reach code that
|
|
||||||
// reads A via a different path, and our local analysis
|
|
||||||
// can't see it.
|
|
||||||
let mut j = i + 1;
|
let mut j = i + 1;
|
||||||
let mut dead = false;
|
let mut dead = false;
|
||||||
|
let mut followed_jmp = false;
|
||||||
while j < instructions.len() {
|
while j < instructions.len() {
|
||||||
let next = &instructions[j];
|
let next = &instructions[j];
|
||||||
// Labels are passive markers.
|
// Labels are passive markers.
|
||||||
|
|
@ -263,8 +264,7 @@ fn remove_dead_loads(instructions: &mut Vec<Instruction>) {
|
||||||
j += 1;
|
j += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Instructions that don't touch A — skip over them.
|
// Memory INC/DEC/STX/STY don't touch A.
|
||||||
// INC/DEC on memory, STX/STY — all leave A alone.
|
|
||||||
if matches!(
|
if matches!(
|
||||||
next.opcode,
|
next.opcode,
|
||||||
Opcode::INC | Opcode::DEC | Opcode::STX | Opcode::STY
|
Opcode::INC | Opcode::DEC | Opcode::STX | Opcode::STY
|
||||||
|
|
@ -273,6 +273,24 @@ fn remove_dead_loads(instructions: &mut Vec<Instruction>) {
|
||||||
j += 1;
|
j += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// Cross one unconditional `JMP <label>` by looking the
|
||||||
|
// target up and resuming scan there. Refuse a second
|
||||||
|
// JMP — the analysis is meant to catch a single
|
||||||
|
// straight-line `loop_body → loop_cond` jump, not an
|
||||||
|
// arbitrary chain.
|
||||||
|
if !followed_jmp && next.opcode == Opcode::JMP {
|
||||||
|
if let AddressingMode::Label(target) = &next.mode {
|
||||||
|
if let Some(target_idx) = instructions.iter().position(|ins| {
|
||||||
|
ins.opcode == Opcode::NOP
|
||||||
|
&& matches!(&ins.mode, AddressingMode::Label(name) if name == target)
|
||||||
|
}) {
|
||||||
|
followed_jmp = true;
|
||||||
|
j = target_idx + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
// Instructions that overwrite A without reading it.
|
// Instructions that overwrite A without reading it.
|
||||||
if matches!(
|
if matches!(
|
||||||
next.opcode,
|
next.opcode,
|
||||||
|
|
@ -281,8 +299,8 @@ fn remove_dead_loads(instructions: &mut Vec<Instruction>) {
|
||||||
dead = true;
|
dead = true;
|
||||||
}
|
}
|
||||||
// Any other instruction — might read A (STA, ADC,
|
// Any other instruction — might read A (STA, ADC,
|
||||||
// SBC, AND, …) or transfer control (JMP, Bxx, JSR,
|
// SBC, AND, JSR …) or transfer control (Bxx, RTS) —
|
||||||
// RTS) — stop scanning and leave the LDA alone.
|
// stop scanning.
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if dead {
|
if dead {
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
a7ce9d12 132084
|
f2dbfbf3 132084
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
d6cf6f66 132084
|
4e8bdb33 132084
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue