Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

7.0KB

Power C — reverse engineering and improvements

Goal: faster code and smaller programs from the Spinnaker Power C compiler on reference/POWERc.d81, without touching the original. Everything here works on copies (reference/POWERc.d81 is only ever read; its SHA-256 is in POWERc.d81.sha256).

Result (all verified in VICE, programs print identical output before/after):

program original improved
vim editor (5 units) 22,506 bytes 19,793 bytes −12.1 %
tests/sem.c (semantics test) 6,428 5,943 −7.5 %
tests/bench.c loop benchmark, jiffies (lower is better) calls 28, array 27, branch 20, arith 30 calls 26, array 17, branch 11, arith 18 −7 % / −37 % / −45 % / −40 %

It also found and fixed a bug in the original runtime: the unsigned >= helper (c0077.obj) has a wrong opcode ($80 instead of $90), so unsigned a, b; r = a >= b; yields 1 for 1 >= 5 (tests/uge.c). Comparisons inside if were unaffected; value contexts were.

Quick start

python reference\powerc-re\tools\setup.py            # once: extract lib objects, build optlib\ (add --disk for POWERc-opt.d81)
powershell -NoProfile -File reference\powerc-re\tools\pcbuild.ps1 -Src hello.c -Name hello -Run '\n'
python reference\powerc-re\tools\regress.py          # optimizer on/off, compares the screens

pcbuild.ps1 compiles with the original cc inside VICE, runs pcopt.py on every .o, links with the original linker using the improved runtime objects, and (with -Run) starts the program and saves the final screen. -NoOpt gives the original behaviour for comparison. Sources are plain ASCII; the scripts convert to PETSCII. Remember Power C is K&R C and cannot type || (see skills/powerc/SKILL.md).

How the toolchain works (reverse engineered)

  • cc.sh runs two passes through temp files: compiler (source → intermediate) and translator (intermediate → .o). The result is native 6502 code, generated by naive template expansion: every value is a 16-bit pair in X (low) / Y (high); temporaries live in a software “temp stack” at $033C up; locals live in zero page from $2B; every operator on non-trivial values calls a tiny runtime helper C$00nn that works on that temp stack.
  • Calls use a software frame ($14/$15 frame pointer, $16/$17 argument pointer, $1A/$1B push stack). C$105 (prolog) and C$106 (epilogue) in c105.obj copy an 8-byte header plus all of the callee's zero-page locals each call — about 450 + 530 cycles, which dominates call-heavy code.
  • C$RETURN (inside c105.obj) is the 5-byte return-value slot; C$0112 copies a value into it.
  • The linker pulls objects by name from the stdlib.l / syslib.l index files (symbol NUL file NUL pairs), so replacing e.g. cmulti.obj on a disk copy replaces that routine in every program.

Object file format (.o / .obj) — tools/objfmt.py round-trips every object on the disk

u16 code_len ; code[]            native code, data (globals, strings) embedded in it
u16 n ; u16 off[n]               opcode offsets of instructions whose 16-bit operand is a code-relative
                                 address (linker adds the load base): local jmp/jsr, pointers to data
u16 n ; { name\0  u8 kind  u16 off }          definitions (functions, globals, string constants)
u16 n ; { name\0  u16 kind u16 site }         references to other objects; site = opcode offset
                                 kind 0 = abs16, 1 = high byte (#>), 2 = low byte (#<), 4 = abs16 + 1
u16 n ; { name\0  u16 size }                  reserved (uninitialised) data blocks

Relocation placeholders in the code are the opcode byte repeated (20 20 20 = jsr extern). The linker overwrites them. Names may contain odd bytes (string constants are <2 garbage>S$<n>).

The tools

file purpose
tools/pcopt.py peephole optimizer for .o files (see below)
tools/objfmt.py object format reader/writer; python objfmt.py x.obj checks the round trip
tools/pcasm.py tiny assembler that writes Power C objects directly (used for src/cmulti.s)
tools/cpu6502.py, helpertest.py, t_*.py, classify.py 6502 emulator + harnesses that prove a rewritten helper behaves exactly like the original (random and edge-case inputs, cycle counts)
tools/dis.py disassemble an object with its relocations/references
tools/pc-compile.ps1, pc-run.ps1, pcbuild.ps1 drive the original cc/link in VICE on throw-away disk copies
src/cmulti.s, src/div_patch.py the replacement runtime routines

What pcopt.py does

It decodes each compiled function (found via the sta $FB prolog and the jmp C$106 epilogue), rewrites it with a liveness analysis (A/X/Y/C/N/Z/V, plus temp-stack memory) and re-emits the object with every offset, branch, relocation and definition fixed up. Anything it cannot decode with certainty is left untouched; with --no-rules it reproduces the input byte for byte.

Rules: inline the <, >, <=, >=, ==, != helpers (signed and unsigned) when they feed an if/loop test, including && chains; return values stored straight into C$RETURN; reload elimination (stx/sty then ldx/ldy of the same slot); ldx #K;ldy #K;stx;sty → lda #K;sta;sta; 16-bit +1 / +K; jump-to-next and branch-over-jump removal; ==/!= against a constant as two branches instead of a branch plus jmp.

Faster runtime routines (optlib/, built by setup.py)

  • cmulti.obj — multiply that needs only 8 shift/add steps when an operand is < 256 (the other 8 steps of the original merely shifted the 32-bit register by one byte); ~40 % faster for small factors, +69 bytes of runtime when * is used.
  • c0047.obj — divide/remainder that normalises the divisor only as far as the dividend needs; same quotient and remainder, e.g. 100/7 is ~2× faster. (30000/3 is ~10 % slower.)
  • c0077.obj — the opcode fix described above.

Safety

Equivalence of the replaced runtime routines is checked in the emulator against the originals over thousands of operand pairs (tools/t_multi.py, t_div.py). The optimizer's rewrites rely on the compiler's temp-stack discipline (operand slots are dead after the operation that consumed them) and on the calling convention (A/X/Y are arguments, flags never are); programs are validated end to end (regress.py, the vim editing scenario). A bug found that way (an && chain read a result slot the inlined compare no longer wrote) is covered by tests/sem.c.

Not done / ideas

  • Prolog/epilogue (c105.obj) — dominates call cost but its frame layout is shared with setjmp/longjmp; micro-optimising the copy loops would buy roughly 10–15 %.
  • A real fix needs a better code generator (patching translator): keep values in registers, stop saving zero-page locals on every call.
  • Inline expansion of C$1102 (push) and the pointer load/store helpers (C$1021…): faster but larger.
  • Arrays/long expressions: only the idioms above are matched, so gains shrink for code that is mostly helper calls.

Powered by TurnKey Linux.