I have spent the past few days making the I6 compiler smarter about skipping dead code. By dead code, I mean lines like this:
if (0) {
print "Text.";
return;
}
The release version of I6 will compile the print and return opcodes, even though the branch is guaranteed to skip over them. With my changes, this whole stanza compiles to nothing.
You’d think that’s pretty silly, but in fact the I7 compiler is sloppy about code generation, so I7 games are full of this stuff.
Also, I6 library code sometimes uses if (CONSTANT)...
to add optional features. With this change, the feature code gets discarded if CONSTANT
is zero.
Why am I posting? Because this is a pretty big change. I had to dig into the code generation logic in funky and complicated ways. (In contrast, the 6.36 code changes were relatively shallow.)
I spent a lot of time diffing assembly dumps to check my work. But I could still have made a mistake. So I’d like some eyes on this change before it gets pushed!
The source code is in this branch on Github. Download, build, test on your favorite I6 source code.
Some notes:
- “Statement can never be reached” warnings are smarter now, but can sometimes appear in the wrong place.
- This change has nothing to do with
#ifdef
. Code which is#ifdef
'd out has always been compiled to nothing. - This change does not optimize out statements like
if (test()) {}
– that is, empty code blocks. - Inline assembly is compiled as written; it won’t be optimized out.
- If you compile with the
-a
(assembly) switch, you’ll see a lot of apparently trivial jumps:
27 +0003d <*> jump L0
30 +00040 .L0
Don’t worry, these get stripped out at backpatch time. You can use txd
to verify this.
If you want more info on the logic of my changes, see this comment in the branch code.
Some statistics for Adventure, a one-room I7 game, and Library of Horror (PunyInform).
Game Code seg Savings
Library.z3 19396 36
Advent.z5 67720 384
Advent.ulx 101802 603
I7-min.z8 278008 856
I7-min.ulx 387062 1206
The second column is the size of the compiled game’s code segment (function code only), when compiling in my branch. The third column is the number of bytes saved over 6.36.
If you’re curious, the main savings in the I6 library comes from the FullScoreSub()
and AttemptToTakeObject()
routines. In FullScoreSub()
, if TASKS_PROVIDED
is 1 (the default value), all the code that prints out task scores can be skipped. In AttemptToTakeObject()
, if SACK_OBJECT
is 0 (the default), all of the putting-in-your-sack-to-make-room code can be skipped.
(Yes, TASKS_PROVIDED
is backwards: 1 means “no tasks”, 0 means “tasks are provided”. Sorry, not my fault.)
Amusing footnote: while checking for stripped code in the I7 game, I noticed that the entire body of FileIO_PutC()
was stripped after the second line. Hey, look, the second line is an unconditional return! That’s a bug that’s been around for years.
In the I6 library, the AnalyseToken()
routine also has a bug – the last two lines can never be reached. This is fairly irrelevant because this routine is only called under Grammar__Version 1, which is obsolete. However, it looks like this bug was picked up by the Metro84 library; see AdjectiveAddress()
here.