|
|
5 天之前 | |
|---|---|---|
| .. | ||
| src | 5 天之前 | |
| tests | 5 天之前 | |
| tools | 5 天之前 | |
| .gitignore | 5 天之前 | |
| POWERc.d81.sha256 | 5 天之前 | |
| README.md | 5 天之前 | |
| cc.sh.prg | 5 天之前 | |
| compiler.prg | 5 天之前 | |
| link.sh.prg | 5 天之前 | |
| shell.prg | 5 天之前 | |
| translator.prg | 5 天之前 | |
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.
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).
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.$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.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..o / .obj) — tools/objfmt.py round-trips every object on the disku16 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>).
| 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 |
pcopt.py doesIt 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.
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.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.
c105.obj) — dominates call cost but its frame layout is shared with
setjmp/longjmp; micro-optimising the copy loops would buy roughly 10–15 %.translator): keep values in registers,
stop saving zero-page locals on every call.C$1102 (push) and the pointer load/store helpers (C$1021…): faster but larger.Powered by TurnKey Linux.