mirror of
https://github.com/AssetRipper/Cpp2IL.git
synced 2026-11-13 20:05:45 -10:00
139 lines
5.6 KiB
C#
139 lines
5.6 KiB
C#
using System.Collections.Generic;
|
|
using Cpp2IL.Core.Graphs;
|
|
using Cpp2IL.Core.ISIL;
|
|
using Cpp2IL.Core.Model.Contexts;
|
|
|
|
namespace Cpp2IL.Core.Analysis;
|
|
|
|
/// <summary>
|
|
/// Removes pure instructions whose result is never used. This eliminates, among other things, the
|
|
/// dead flag/temporary computations the x86 lifter emits eagerly for every comparison - a single
|
|
/// <c>cmp</c>/<c>test</c> produces all of CF/OF/SF/ZF/PF plus scratch temporaries, but the branch
|
|
/// that follows only consumes one of them.
|
|
///
|
|
/// Must run while the graph is still in SSA form (every local is assigned exactly once), so that a
|
|
/// global use count of zero is sufficient to prove a definition dead. Instructions are turned into
|
|
/// nops rather than spliced out; the structural cleanup happens later, out of SSA, where it is safe
|
|
/// for phi nodes.
|
|
/// </summary>
|
|
public static class DeadCodeEliminator
|
|
{
|
|
public static void Run(MethodAnalysisContext method) => Run(method.ControlFlowGraph!);
|
|
|
|
public static void Run(ISILControlFlowGraph cfg)
|
|
{
|
|
// Removing a dead definition can make its operands dead in turn, so iterate to a fixpoint.
|
|
// This is monotonic (each pass only nops instructions) and therefore always terminates.
|
|
var changed = true;
|
|
while (changed)
|
|
{
|
|
changed = false;
|
|
|
|
var useCounts = CountUses(cfg);
|
|
|
|
foreach (var block in cfg.Blocks)
|
|
{
|
|
foreach (var instruction in block.Instructions)
|
|
{
|
|
if (!IsRemovable(instruction.OpCode))
|
|
continue;
|
|
|
|
// Only definitions of a register local are candidates. Stores have a memory or
|
|
// field destination (Destination is not a local) and are never dead.
|
|
if (instruction.Destination is not LocalVariable destination)
|
|
continue;
|
|
|
|
if (useCounts.TryGetValue(destination, out var count) && count > 0)
|
|
continue;
|
|
|
|
instruction.OpCode = OpCode.Nop;
|
|
instruction.SetOperands();
|
|
changed = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static Dictionary<LocalVariable, int> CountUses(ISILControlFlowGraph cfg)
|
|
{
|
|
var counts = new Dictionary<LocalVariable, int>();
|
|
|
|
foreach (var block in cfg.Blocks)
|
|
foreach (var instruction in block.Instructions)
|
|
foreach (var used in UsedLocals(instruction))
|
|
counts[used] = counts.TryGetValue(used, out var c) ? c + 1 : 1;
|
|
|
|
return counts;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Every local read by the instruction. The single write position - a plain local destination -
|
|
/// is excluded. Memory and field operands always contribute their address/object locals as
|
|
/// reads, even when they are the destination of a store.
|
|
/// </summary>
|
|
private static IEnumerable<LocalVariable> UsedLocals(Instruction instruction)
|
|
{
|
|
var destination = instruction.Destination as LocalVariable;
|
|
|
|
foreach (var operand in instruction.Operands)
|
|
{
|
|
switch (operand)
|
|
{
|
|
case LocalVariable local when !ReferenceEquals(local, destination):
|
|
yield return local;
|
|
break;
|
|
case MemoryOperand memory:
|
|
if (memory.Base is LocalVariable baseLocal)
|
|
yield return baseLocal;
|
|
if (memory.Index is LocalVariable indexLocal)
|
|
yield return indexLocal;
|
|
break;
|
|
// A static field access doesn't read the storage pointer it was resolved from, so that
|
|
// pointer (and the class load feeding it) is free to die.
|
|
case FieldReference { Field.IsStatic: false, Local: { } fieldLocal }:
|
|
yield return fieldLocal;
|
|
break;
|
|
// Handing out a slot's address is a read of it as far as we can tell, whatever the callee then does with it.
|
|
case AddressOf { Target: LocalVariable addressed }:
|
|
yield return addressed;
|
|
break;
|
|
case AddressOf { Target: ArrayAccess addressedElement }:
|
|
foreach (var used in ArrayAccessLocals(addressedElement))
|
|
yield return used;
|
|
break;
|
|
case ArrayAccess access:
|
|
foreach (var used in ArrayAccessLocals(access))
|
|
yield return used;
|
|
break;
|
|
case ArrayLength { Array: { } lengthArray }:
|
|
yield return lengthArray;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static IEnumerable<LocalVariable> ArrayAccessLocals(ArrayAccess access)
|
|
{
|
|
yield return access.Array;
|
|
|
|
if (access.Index is LocalVariable index)
|
|
yield return index;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Opcodes with no side effects, so removing a never-read result is safe. Calls, stores,
|
|
/// returns and branches are intentionally excluded.
|
|
/// </summary>
|
|
private static bool IsRemovable(OpCode opCode) =>
|
|
opCode switch
|
|
{
|
|
OpCode.Move or OpCode.Phi
|
|
or OpCode.Add or OpCode.Subtract or OpCode.Multiply or OpCode.Divide or OpCode.Modulo
|
|
or OpCode.ShiftLeft or OpCode.ShiftRight
|
|
or OpCode.And or OpCode.Or or OpCode.Xor
|
|
or OpCode.Not or OpCode.Negate=> true,
|
|
>= OpCode.CheckEqual and <= OpCode.CheckLessOrEqual => true,
|
|
_ => false
|
|
};
|
|
}
|