Skip to content

Commit

Permalink
optimizer removes redundant DUP and DROP; throw when optimizer fails (#…
Browse files Browse the repository at this point in the history
…1168)

* optimizer removes redundant DUP and DROP

* fix DUP reference from jumps

* cancel cloning debugInfo
  • Loading branch information
Hecate2 committed Sep 17, 2024
1 parent 9be7f36 commit 11a01b2
Show file tree
Hide file tree
Showing 63 changed files with 483 additions and 282 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,9 @@ internal void Compile()
catch (Exception ex)
{
Console.Error.WriteLine($"Failed to optimize: {ex}");
Console.Error.WriteLine("Please try again without using the experimental optimizer.");
Console.Error.WriteLine($"e.g. --{nameof(Options.Optimize).ToLower()}={CompilationOptions.OptimizationType.Basic}");
throw;
}
}

Expand Down
18 changes: 10 additions & 8 deletions src/Neo.Compiler.CSharp/Optimizer/Analysers/BasicBlock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,20 +62,22 @@ public static Dictionary<int, Dictionary<int, Instruction>> BasicBlocksInDict(Ne

public List<BasicBlock> basicBlocks;
public Dictionary<Instruction, BasicBlock> basicBlocksByStartInstruction;
public InstructionCoverage coverage;
public IEnumerable<(int startAddr, List<Instruction> block)> sortedBasicBlocks;
public ContractManifest manifest;
public JToken? debugInfo;
public ContractInBasicBlocks(NefFile nef, ContractManifest manifest, JToken? debugInfo = null)
{
this.manifest = manifest;
this.debugInfo = debugInfo;
InstructionCoverage coverage = new(nef, manifest);
IEnumerable<(int startAddr, List<Instruction> block)> sortedBasicBlocks =
from kv in coverage.basicBlocksInDict
orderby kv.Key ascending
select (kv.Key,
// kv.Value sorted by address
(from singleBlockKv in kv.Value orderby singleBlockKv.Key ascending select singleBlockKv.Value).ToList()
);
coverage = new(nef, manifest);
sortedBasicBlocks =
(from kv in coverage.basicBlocksInDict
orderby kv.Key ascending
select (kv.Key,
// kv.Value sorted by address
(from singleBlockKv in kv.Value orderby singleBlockKv.Key ascending select singleBlockKv.Value).ToList()
));
basicBlocksByStartInstruction = new();
BasicBlock? prevBlock = null;
// build all blocks without handling jumps between blocks
Expand Down
2 changes: 1 addition & 1 deletion src/Neo.Compiler.CSharp/Optimizer/Analysers/EntryPoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public static Dictionary<int, EntryType> EntryPointsByCallA(NefFile nef)
{
int target = JumpTarget.ComputeJumpTarget(addr, instruction);
if (target != addr && target >= 0)
result.Add(target, EntryType.PUSHA);
result[target] = EntryType.PUSHA;
}
return result;
}
Expand Down
18 changes: 12 additions & 6 deletions src/Neo.Compiler.CSharp/Optimizer/Analysers/OpCodeTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,9 @@ static OpCodeTypes()
JMPLE_L,
};

public static readonly HashSet<OpCode> loadStaticFieldsConst = new()
public static readonly HashSet<OpCode> loadStaticFields = new()
{
LDSFLD,
LDSFLD0,
LDSFLD1,
LDSFLD2,
Expand All @@ -160,8 +161,9 @@ static OpCodeTypes()
LDSFLD5,
LDSFLD6,
};
public static readonly HashSet<OpCode> storeStaticFieldsConst = new()
public static readonly HashSet<OpCode> storeStaticFields = new()
{
STSFLD,
STSFLD0,
STSFLD1,
STSFLD2,
Expand All @@ -170,8 +172,9 @@ static OpCodeTypes()
STSFLD5,
STSFLD6,
};
public static readonly HashSet<OpCode> loadLocalVariablesConst = new()
public static readonly HashSet<OpCode> loadLocalVariables = new()
{
LDLOC,
LDLOC0,
LDLOC1,
LDLOC2,
Expand All @@ -180,8 +183,9 @@ static OpCodeTypes()
LDLOC5,
LDLOC6,
};
public static readonly HashSet<OpCode> storeLocalVariablesConst = new()
public static readonly HashSet<OpCode> storeLocalVariables = new()
{
STLOC,
STLOC0,
STLOC1,
STLOC2,
Expand All @@ -190,8 +194,9 @@ static OpCodeTypes()
STLOC5,
STLOC6,
};
public static readonly HashSet<OpCode> loadArgumentsConst = new()
public static readonly HashSet<OpCode> loadArguments = new()
{
LDARG,
LDARG0,
LDARG1,
LDARG2,
Expand All @@ -200,8 +205,9 @@ static OpCodeTypes()
LDARG5,
LDARG6,
};
public static readonly HashSet<OpCode> storeArgumentsConst = new()
public static readonly HashSet<OpCode> storeArguments = new()
{
STARG,
STARG0,
STARG1,
STARG2,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,39 @@ public static Script BuildScriptWithJumpTargets(
Script script = new(simplifiedScript.ToArray());
return script;
}

/// <summary>
/// Typically used when you delete the oldTarget from script
/// and the newTarget is the first following instruction undeleted in script
/// </summary>
/// <param name="oldTarget"></param>
/// <param name="newTarget"></param>
/// <param name="jumpSourceToTargets"></param>
/// <param name="jumpTargetToSources"></param>
/// <param name="trySourceToTargets"></param>
public static void RetargetJump(Instruction oldTarget, Instruction newTarget,
Dictionary<Instruction, Instruction> jumpSourceToTargets,
Dictionary<Instruction, HashSet<Instruction>> jumpTargetToSources,
Dictionary<Instruction, (Instruction, Instruction)> trySourceToTargets)
{
if (jumpTargetToSources.Remove(oldTarget, out HashSet<Instruction>? sources))
{
foreach (Instruction s in sources)
{
if (jumpSourceToTargets.TryGetValue(s, out Instruction? t0) && t0 == oldTarget)
jumpSourceToTargets[s] = newTarget;
if (trySourceToTargets.TryGetValue(s, out (Instruction t1, Instruction t2) t))
{
Instruction newT1 = (t.t1 == oldTarget ? newTarget : t.t1);
Instruction newT2 = (t.t2 == oldTarget ? newTarget : t.t2);
trySourceToTargets[s] = (newT1, newT2);
}
}
if (jumpTargetToSources.TryGetValue(newTarget, out HashSet<Instruction>? newTargetSources))
newTargetSources.Union(sources);
else
jumpTargetToSources[newTarget] = sources;
}
}
}
}
2 changes: 1 addition & 1 deletion src/Neo.Compiler.CSharp/Optimizer/Strategies/Optimizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,10 @@ public static (NefFile, ContractManifest, JObject?) Optimize(NefFile nef, Contra
manifest.Extra["nef"] = new JObject();
manifest.Extra["nef"]!["optimization"] = optimizationType.ToString();
// TODO in the future: optimize by StrategyAttribute in a loop
debugInfo = debugInfo?.Clone() as JObject; // do not pollute the input when optimization fails
(nef, manifest, debugInfo) = Reachability.RemoveUnnecessaryJumps(nef, manifest, debugInfo);
(nef, manifest, debugInfo) = Reachability.ReplaceJumpWithRet(nef, manifest, debugInfo);
(nef, manifest, debugInfo) = Reachability.RemoveUncoveredInstructions(nef, manifest, debugInfo);
(nef, manifest, debugInfo) = Peephole.RemoveDupDrop(nef, manifest, debugInfo);
(nef, manifest, debugInfo) = Reachability.RemoveUnnecessaryJumps(nef, manifest, debugInfo);
(nef, manifest, debugInfo) = Reachability.ReplaceJumpWithRet(nef, manifest, debugInfo);
return (nef, manifest, debugInfo);
Expand Down
92 changes: 92 additions & 0 deletions src/Neo.Compiler.CSharp/Optimizer/Strategies/Peephole.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Copyright (C) 2015-2024 The Neo Project.
//
// Reachability.cs file belongs to the neo project and is free
// software distributed under the MIT software license, see the
// accompanying file LICENSE in the main directory of the
// repository or http://www.opensource.org/licenses/mit-license.php
// for more details.
//
// Redistribution and use in source and binary forms with or without
// modifications are permitted.

using Neo.Json;
using Neo.SmartContract;
using Neo.SmartContract.Manifest;
using Neo.VM;
using System.Collections.Generic;
using System.Linq;

namespace Neo.Optimizer
{
public static class Peephole
{
public static HashSet<OpCode> RemoveDupDropOpCodes = new() { OpCode.REVERSEITEMS, OpCode.CLEARITEMS, OpCode.DUP, OpCode.DROP, OpCode.ABORTMSG };

/// <summary>
/// DUP SOMEOP DROP
/// delete DUP and DROP when they are meaningless, optimizing to SOMPOP only
/// This is mainly used for simple assignments like `a=1`, which is compiled to
/// PUSH1 DUP STLOC:$index_of_a DROP
/// This is correct compilation, because the expression `a=1` has return value 1
/// The return value of assignment expression is used in continuous assignments like `a=b=1`
/// But at runtime we just need PUSH1 STLOC:$index_of_a
/// TODO in the future: use symbolic VM to judge multiple instructions between DUP and DROP
/// </summary>
/// <param name="nef"></param>
/// <param name="manifest"></param>
/// <param name="debugInfo"></param>
/// <returns></returns>
[Strategy(Priority = 1 << 10)]
public static (NefFile, ContractManifest, JObject?) RemoveDupDrop(NefFile nef, ContractManifest manifest, JObject? debugInfo = null)
{
ContractInBasicBlocks contractInBasicBlocks = new(nef, manifest, debugInfo);
InstructionCoverage oldContractCoverage = contractInBasicBlocks.coverage;
Dictionary<int, Instruction> oldAddressToInstruction = new();
foreach ((int a, Instruction i) in oldContractCoverage.addressAndInstructions)
oldAddressToInstruction.Add(a, i);
(Dictionary<Instruction, Instruction> jumpSourceToTargets,
Dictionary<Instruction, (Instruction, Instruction)> trySourceToTargets,
Dictionary<Instruction, HashSet<Instruction>> jumpTargetToSources) =
(oldContractCoverage.jumpInstructionSourceToTargets,
oldContractCoverage.tryInstructionSourceToTargets,
oldContractCoverage.jumpTargetToSources);
System.Collections.Specialized.OrderedDictionary simplifiedInstructionsToAddress = new();
int currentAddress = 0;
foreach (List<Instruction> basicBlock in contractInBasicBlocks.sortedBasicBlocks.Select(i => i.block))
{
for (int index = 0; index < basicBlock.Count; index++)
{
if (index + 2 < basicBlock.Count
&& basicBlock[index].OpCode == OpCode.DUP
&& basicBlock[index + 2].OpCode == OpCode.DROP)
{
Instruction currentDup = basicBlock[index];
Instruction nextInstruction = basicBlock[index + 1];
OpCode opAfterDup = nextInstruction.OpCode;
if (OpCodeTypes.storeArguments.Contains(opAfterDup)
|| OpCodeTypes.storeStaticFields.Contains(opAfterDup)
|| OpCodeTypes.storeLocalVariables.Contains(opAfterDup)
|| RemoveDupDropOpCodes.Contains(opAfterDup))
{
// Include only the instruction between DUP and DROP
simplifiedInstructionsToAddress.Add(nextInstruction, currentAddress);
currentAddress += nextInstruction.Size;
index += 2;

// If the old DUP is target of jump, re-target to the next instruction
OptimizedScriptBuilder.RetargetJump(currentDup, nextInstruction,
jumpSourceToTargets, jumpTargetToSources, trySourceToTargets);
continue;
}
}
simplifiedInstructionsToAddress.Add(basicBlock[index], currentAddress);
currentAddress += basicBlock[index].Size;
}
}
return AssetBuilder.BuildOptimizedAssets(nef, manifest, debugInfo,
simplifiedInstructionsToAddress,
jumpSourceToTargets, trySourceToTargets,
oldAddressToInstruction);
}
}
}
43 changes: 9 additions & 34 deletions src/Neo.Compiler.CSharp/Optimizer/Strategies/Reachability.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,23 +89,13 @@ public static (NefFile, ContractManifest, JObject?) RemoveUnnecessaryJumps(NefFi
// This is unnecessary jump. The jump should be deleted.
// And, if this JMP is the target of other jump instructions,
// re-target to the next instruction after this JMP.
if (jumpTargetToSources.Remove(i, out HashSet<Instruction>? sources))
{
Instruction nextInstruction = oldAddressToInstruction[a + i.Size];
foreach (Instruction s in sources)
{
if (jumpSourceToTargets.TryGetValue(s, out Instruction? t0) && t0 == i)
jumpSourceToTargets[s] = nextInstruction;
if (trySourceToTargets.TryGetValue(s, out (Instruction t1, Instruction t2) t))
{
Instruction newT1 = (t.t1 == i ? nextInstruction : t.t1);
Instruction newT2 = (t.t2 == i ? nextInstruction : t.t2);
trySourceToTargets[s] = (newT1, newT2);
}
}

jumpTargetToSources[nextInstruction] = sources;
}
Instruction nextInstruction = oldAddressToInstruction[a + i.Size];
// handle the reference of the deleted JMP
jumpSourceToTargets.Remove(i);
jumpTargetToSources[nextInstruction].Remove(i);
if (jumpTargetToSources[nextInstruction].Count == 0)
jumpTargetToSources.Remove(nextInstruction);
OptimizedScriptBuilder.RetargetJump(i, nextInstruction, jumpSourceToTargets, jumpTargetToSources, trySourceToTargets);
continue; // do not add this JMP into simplified instructions
}
}
Expand Down Expand Up @@ -160,23 +150,8 @@ public static (NefFile, ContractManifest, JObject?) ReplaceJumpWithRet(NefFile n
// handle the reference of the added RET
Instruction newRet = new Script(new byte[] { (byte)OpCode.RET }).GetInstruction(0);
// above is a workaround of new Instruction(OpCode.RET)
if (jumpTargetToSources.TryGetValue(i, out HashSet<Instruction>? othersJumpingToCurrentJmp))
{
foreach (Instruction iJumpingToCurrentRet in othersJumpingToCurrentJmp)
{
if (SingleJumpInOperand(iJumpingToCurrentRet))
jumpSourceToTargets[iJumpingToCurrentRet] = newRet;
if (iJumpingToCurrentRet.OpCode == OpCode.TRY || iJumpingToCurrentRet.OpCode == OpCode.TRY_L)
{
(Instruction t1, Instruction t2) = trySourceToTargets[iJumpingToCurrentRet];
if (t1 == i) t1 = newRet;
if (t2 == i) t2 = newRet;
trySourceToTargets[iJumpingToCurrentRet] = (t1, t2);
}
}
jumpTargetToSources.Remove(i);
jumpTargetToSources[newRet] = othersJumpingToCurrentJmp;
}
OptimizedScriptBuilder.RetargetJump(i, newRet,
jumpSourceToTargets, jumpTargetToSources, trySourceToTargets);
simplifiedInstructionsToAddress.Add(newRet, currentAddress);
currentAddress += newRet.Size;
continue;
Expand Down
26 changes: 26 additions & 0 deletions tests/Neo.Compiler.CSharp.TestContracts/Contract_Assignment.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Neo.SmartContract.Framework;

namespace Neo.Compiler.CSharp.TestContracts
{
public class Contract_Assignment : SmartContract.Framework.SmartContract
{
public static void TestAssignment()
{
int a = 1;
ExecutionEngine.Assert(a == 1);
int b;
a = b = 2;
ExecutionEngine.Assert(a == 2);
ExecutionEngine.Assert(b == 2);
}

public static void TestCoalesceAssignment()
{
int? a = null;
a ??= 1;
ExecutionEngine.Assert(a == 1);
a ??= 2;
ExecutionEngine.Assert(a == 1);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace Neo.Compiler.CSharp.TestContracts
{
[Contract("54a484c3f3c4a46445a28dd70bc35f6cf917da60")]
[Contract("0e26a6a9b6f37a54d5666aaa2efb71dc75abfdfa")]
public class Contract_Call
{
#pragma warning disable CS0626 // Method, operator, or accessor is marked external and has no attributes on it
Expand Down
Loading

0 comments on commit 11a01b2

Please sign in to comment.