Skip to content

Commit 14e0335

Browse files
committed
Decompiler: add SSA dead-code elimination
1 parent 676ff28 commit 14e0335

3 files changed

Lines changed: 192 additions & 0 deletions

File tree

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using Cpp2IL.Core.Analysis;
4+
using Cpp2IL.Core.Graphs;
5+
using Cpp2IL.Core.ISIL;
6+
7+
namespace Cpp2IL.Core.Tests.Analysis;
8+
9+
public class DeadCodeEliminationTests
10+
{
11+
private static List<Instruction> Live(ISILControlFlowGraph graph)
12+
=> graph.Blocks.SelectMany(b => b.Instructions).Where(i => i.OpCode != OpCode.Nop).ToList();
13+
14+
[Test]
15+
public void RemovesDeadDefinitionButKeepsLiveOnes()
16+
{
17+
var x = new LocalVariable("x", new Register(null, "x"));
18+
var dead = new LocalVariable("dead", new Register(null, "dead"));
19+
20+
var graph = new ISILControlFlowGraph(new List<Instruction>
21+
{
22+
new(0, OpCode.Move, x, 5),
23+
new(1, OpCode.Subtract, dead, x, 1), // dead's result is never read
24+
new(2, OpCode.Return, x),
25+
});
26+
27+
DeadCodeEliminator.Run(graph);
28+
29+
var live = Live(graph);
30+
Assert.That(live.Any(i => i.OpCode == OpCode.Subtract), Is.False, "dead computation should be removed");
31+
Assert.That(live.Any(i => i.OpCode == OpCode.Move), Is.True, "live definition should remain");
32+
Assert.That(live.Any(i => i.OpCode == OpCode.Return), Is.True, "terminator should remain");
33+
}
34+
35+
[Test]
36+
public void RemovesDeadChainToFixpoint()
37+
{
38+
// x = 5; temp = x - 1; flag = temp < 0; return x
39+
// 'flag' is unused -> dead; that makes 'temp' unused -> dead too (cascade).
40+
var x = new LocalVariable("x", new Register(null, "x"));
41+
var temp = new LocalVariable("temp", new Register(null, "temp"));
42+
var flag = new LocalVariable("flag", new Register(null, "flag"));
43+
44+
var graph = new ISILControlFlowGraph(new List<Instruction>
45+
{
46+
new(0, OpCode.Move, x, 5),
47+
new(1, OpCode.Subtract, temp, x, 1),
48+
new(2, OpCode.CheckLess, flag, temp, 0),
49+
new(3, OpCode.Return, x),
50+
});
51+
52+
DeadCodeEliminator.Run(graph);
53+
54+
var live = Live(graph);
55+
Assert.That(live.Any(i => i.OpCode == OpCode.CheckLess), Is.False, "dead flag should be removed");
56+
Assert.That(live.Any(i => i.OpCode == OpCode.Subtract), Is.False, "now-dead temp should be removed (cascade)");
57+
Assert.That(live.Count, Is.EqualTo(2), "only the live Move and Return should remain");
58+
}
59+
60+
[Test]
61+
public void KeepsCallsEvenWhenResultUnused()
62+
{
63+
// A call with no observed result must be kept (side effects).
64+
var graph = new ISILControlFlowGraph(new List<Instruction>
65+
{
66+
new(0, OpCode.CallVoid, 0xDEADBEEFUL),
67+
new(1, OpCode.Return),
68+
});
69+
70+
DeadCodeEliminator.Run(graph);
71+
72+
Assert.That(Live(graph).Any(i => i.OpCode == OpCode.CallVoid), Is.True, "calls must never be removed");
73+
}
74+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
using System.Collections.Generic;
2+
using Cpp2IL.Core.Graphs;
3+
using Cpp2IL.Core.ISIL;
4+
using Cpp2IL.Core.Model.Contexts;
5+
6+
namespace Cpp2IL.Core.Analysis;
7+
8+
/// <summary>
9+
/// Removes pure instructions whose result is never used. This eliminates, among other things, the
10+
/// dead flag/temporary computations the x86 lifter emits eagerly for every comparison - a single
11+
/// <c>cmp</c>/<c>test</c> produces all of CF/OF/SF/ZF/PF plus scratch temporaries, but the branch
12+
/// that follows only consumes one of them.
13+
///
14+
/// Must run while the graph is still in SSA form (every local is assigned exactly once), so that a
15+
/// global use count of zero is sufficient to prove a definition dead. Instructions are turned into
16+
/// nops rather than spliced out; the structural cleanup happens later, out of SSA, where it is safe
17+
/// for phi nodes.
18+
/// </summary>
19+
public static class DeadCodeEliminator
20+
{
21+
public static void Run(MethodAnalysisContext method) => Run(method.ControlFlowGraph!);
22+
23+
public static void Run(ISILControlFlowGraph cfg)
24+
{
25+
// Removing a dead definition can make its operands dead in turn, so iterate to a fixpoint.
26+
// This is monotonic (each pass only nops instructions) and therefore always terminates.
27+
var changed = true;
28+
while (changed)
29+
{
30+
changed = false;
31+
32+
var useCounts = CountUses(cfg);
33+
34+
foreach (var block in cfg.Blocks)
35+
{
36+
foreach (var instruction in block.Instructions)
37+
{
38+
if (!IsRemovable(instruction.OpCode))
39+
continue;
40+
41+
// Only definitions of a register local are candidates. Stores have a memory or
42+
// field destination (Destination is not a local) and are never dead.
43+
if (instruction.Destination is not LocalVariable destination)
44+
continue;
45+
46+
if (useCounts.TryGetValue(destination, out var count) && count > 0)
47+
continue;
48+
49+
instruction.OpCode = OpCode.Nop;
50+
instruction.Operands = [];
51+
changed = true;
52+
}
53+
}
54+
}
55+
}
56+
57+
private static Dictionary<LocalVariable, int> CountUses(ISILControlFlowGraph cfg)
58+
{
59+
var counts = new Dictionary<LocalVariable, int>();
60+
61+
foreach (var block in cfg.Blocks)
62+
foreach (var instruction in block.Instructions)
63+
foreach (var used in UsedLocals(instruction))
64+
counts[used] = counts.TryGetValue(used, out var c) ? c + 1 : 1;
65+
66+
return counts;
67+
}
68+
69+
/// <summary>
70+
/// Every local read by the instruction. The single write position - a plain local destination -
71+
/// is excluded. Memory and field operands always contribute their address/object locals as
72+
/// reads, even when they are the destination of a store.
73+
/// </summary>
74+
private static IEnumerable<LocalVariable> UsedLocals(Instruction instruction)
75+
{
76+
var destination = instruction.Destination as LocalVariable;
77+
78+
foreach (var operand in instruction.Operands)
79+
{
80+
switch (operand)
81+
{
82+
case LocalVariable local when !ReferenceEquals(local, destination):
83+
yield return local;
84+
break;
85+
case MemoryOperand memory:
86+
if (memory.Base is LocalVariable baseLocal)
87+
yield return baseLocal;
88+
if (memory.Index is LocalVariable indexLocal)
89+
yield return indexLocal;
90+
break;
91+
case FieldReference field when field.Local is { } fieldLocal:
92+
yield return fieldLocal;
93+
break;
94+
}
95+
}
96+
}
97+
98+
/// <summary>
99+
/// Opcodes with no side effects, so removing a never-read result is safe. Calls, stores,
100+
/// returns and branches are intentionally excluded.
101+
/// </summary>
102+
private static bool IsRemovable(OpCode opCode) =>
103+
opCode switch
104+
{
105+
OpCode.Move or OpCode.Phi
106+
or OpCode.Add or OpCode.Subtract or OpCode.Multiply or OpCode.Divide
107+
or OpCode.ShiftLeft or OpCode.ShiftRight
108+
or OpCode.And or OpCode.Or or OpCode.Xor
109+
or OpCode.Not or OpCode.Negate
110+
or OpCode.CheckEqual or OpCode.CheckGreater or OpCode.CheckLess => true,
111+
_ => false
112+
};
113+
}

Cpp2IL.Core/Model/Contexts/MethodAnalysisContext.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,11 @@ public void Analyze()
356356
// Create locals
357357
SsaForm.Build(this);
358358
LocalVariables.CreateAll(this);
359+
360+
// Eliminate dead computations (e.g. the unused flags emitted for every comparison) while
361+
// still in SSA form, where a zero use count proves a definition dead.
362+
DeadCodeEliminator.Run(this);
363+
359364
SsaForm.Remove(this);
360365

361366
MetadataResolver.ResolveAll(this);

0 commit comments

Comments
 (0)