Skip to content

CFG Part 0: Max Stack Size & Peephole Optimizer Fixes/Improvements#2561

Draft
ike709 wants to merge 16 commits into
OpenDreamProject:masterfrom
ike709:peepfix
Draft

CFG Part 0: Max Stack Size & Peephole Optimizer Fixes/Improvements#2561
ike709 wants to merge 16 commits into
OpenDreamProject:masterfrom
ike709:peepfix

Conversation

@ike709

@ike709 ike709 commented May 9, 2026

Copy link
Copy Markdown
Collaborator

This PR does a few things:

  • Steals some redundant Jump optimizations from Control Flow Graph #2558
  • Fixes an issue with a case in which we did not rescan opcodes after calling AttemptCurrentOpt()
  • Changes the peephole optimizer to keep rerunning until each pass reports no changes to the bytecode. This provides further gains in some operations involving stack pushes and list creations, and generally makes it less annoying than trying to spread the optimizations out across separate passes in the correct order. This could also lead to further simplification of the peephole optimizer in the future, but I didn't want to change too much all at once.
  • Fixes some incorrect stack size/delta info
  • Fixes max stack size not being recomputed after optimizations. This leads to reductions in stack size across thousands of procs.
  • switch() cases no longer emit a Jump instruction if the last operation is a terminal change in control flow (e.g. return)
  • spawn() blocks no longer emit PushNull and Return if the last operation is a terminal change in control flow (e.g. return)
  • Peephole optimizer now ignores AnnotatedBytecodeVariable during opt scanning
  • Pointless Jump instructions are more aggressively pruned by the bytecode optimizer
  • Made some fixes to ProcDecoder that arose during testing

I did before & after diffs on /tg/station compilation & disassembly to confirm that this resulted in further optimizations to the bytecode without introducing any apparent regressions. The NullRef opt in particular was being left on the table in thousands of cases, while the other peephole optimizer changes are less dramatic but still present.

The fixes to max stack size are also a huge improvement. Thousands of procs have either had their max stack size reduced to 0 or have seen a reduction from double to single digits. I ran TG through round init to confirm nothing exploded.

This sufficiently beats the codegen and optimizer into submission to the point that we can now implement dead code detection (after CFG is finished) and run it on /tg/station and goonstation without getting false positives.

@boring-cyborg boring-cyborg Bot added the Compiler Involves the OpenDream compiler label May 9, 2026
@ike709 ike709 requested a review from wixoaGit May 9, 2026 16:00
@github-actions github-actions Bot added the size/M label May 9, 2026
@ike709 ike709 changed the title Peephole Optimizer Fixes & Improvements Max Stack Size & Peephole Optimizer Fixes/Improvements May 9, 2026
@github-actions github-actions Bot added size/L and removed size/M labels May 9, 2026
@ike709

ike709 commented May 10, 2026

Copy link
Copy Markdown
Collaborator Author

I'm calling this PR done barring any review comments. I did another testing pass without any of my other non-committed experimental changes, and confirmed that TG and goon both compile, while also making sure I can start a round on TG.

@ike709 ike709 changed the title Max Stack Size & Peephole Optimizer Fixes/Improvements CFG Part 0: Max Stack Size & Peephole Optimizer Fixes/Improvements May 10, 2026
@boring-cyborg boring-cyborg Bot added the Runtime Involves the OpenDream server/runtime label May 10, 2026
@github-actions

Copy link
Copy Markdown

This pull request has conflicts, please resolve those before we can evaluate the pull request.

Comment thread DMCompiler/DM/DMProc.cs
public bool LastInstructionTransfersControl() {
List<IAnnotatedBytecode> bytecode = AnnotatedBytecode.GetAnnotatedBytecode();
HashSet<string>? referencedLabels = null;
// Man sometimes it'd be nice if we had a list of just instructions and didn't need loops like this just to grab the last actual instruction

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could also save the last opcode written by WriteOpcode()

Comment thread DMCompiler/DM/DMProc.cs
continue;
case AnnotatedBytecodeLabel label:
referencedLabels ??= GetReferencedLabels(bytecode);
if (referencedLabels is not null && referencedLabels.Contains(label.LabelName))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't this not work if the label is only referenced later on in the code?

ProcessBlockInner(statement.Body);
proc.EndScope();
proc.Jump(endLabel);
if (!proc.LastInstructionTransfersControl())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could LastInstructionTransfersControl() be put into Jump() instead of littering these checks everywhere?

/// Recomputes the maximum possible stack size from the current annotated bytecode.
/// Used after optimization because peephole rewrites can change the max stack size.
/// </summary>
public int RecalculateMaxStackSize() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why bother tracking stack size while generating opcodes if we just recalculate it at the end?

foreach (var optPass in _passes) {
changed |= RunPass((byte)optPass, input);
}
} while (changed);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds a second pass to practically every proc at minimum. I'd like to avoid that if possible, what additional optimizations is this catching?

if (currentOpt.Optimization?.CheckPreconditions(input, i - optSize) is true) {
currentOpt.Optimization.Apply(_compiler, input, i - optSize);
int startIndex = currentOptStartIndex;
var skippedVariables = RemoveTransparentVariables(input, startIndex);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing every AnnotatedBytecodeVariable surrounding every instruction, checking for a possible optimization, then inserting those variables back, seems ridiculous. Is there no better way to do this?

Not to mention all the O(n) operations this introduces.

RewriteLabelAliases(input, labelAliases);
}

private Dictionary<string, string>? RemoveJumpsAfterTerminalInstructions(List<IAnnotatedBytecode> input, Dictionary<string, string>? labelAliases) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we just avoid emitting these in DMProc in the first place, instead of looping through every opcode and potentially running multiple O(n) removals?

@ike709 ike709 marked this pull request as draft June 14, 2026 06:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Compiler Involves the OpenDream compiler Runtime Involves the OpenDream server/runtime size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants