From 4e2cf2b23547c252beeec5a6b17224f78f96132a Mon Sep 17 00:00:00 2001 From: ascpixi <44982772+ascpixi@users.noreply.github.com> Date: Fri, 14 Oct 2022 16:35:38 +0200 Subject: [PATCH 1/3] Create optimizer infrastructure --- source/Cosmos.IL2CPU/AppAssembler.cs | 2 +- source/Cosmos.IL2CPU/ILReader.cs | 47 +++++- source/Cosmos.IL2CPU/ILScanner.cs | 7 + .../IOptimizerLookaheadProvider.cs | 28 ++++ .../Cosmos.IL2CPU/Optimization/Optimizer.cs | 74 +++++++++ .../Optimization/OptimizerPass.cs | 26 +++ .../Passes/InlineDirectPropertiesPass.cs | 152 ++++++++++++++++++ source/Cosmos.IL2CPU/Optimizer.cs | 73 --------- 8 files changed, 330 insertions(+), 79 deletions(-) create mode 100644 source/Cosmos.IL2CPU/Optimization/IOptimizerLookaheadProvider.cs create mode 100644 source/Cosmos.IL2CPU/Optimization/Optimizer.cs create mode 100644 source/Cosmos.IL2CPU/Optimization/OptimizerPass.cs create mode 100644 source/Cosmos.IL2CPU/Optimization/Passes/InlineDirectPropertiesPass.cs delete mode 100644 source/Cosmos.IL2CPU/Optimizer.cs diff --git a/source/Cosmos.IL2CPU/AppAssembler.cs b/source/Cosmos.IL2CPU/AppAssembler.cs index 29acaab82..203d1a6da 100644 --- a/source/Cosmos.IL2CPU/AppAssembler.cs +++ b/source/Cosmos.IL2CPU/AppAssembler.cs @@ -1379,7 +1379,7 @@ public void EmitEntrypoint(MethodBase aEntrypoint, MethodBase[] aBootEntries = n if (ShouldOptimize) { - Optimizer.Optimize(Assembler); + } } diff --git a/source/Cosmos.IL2CPU/ILReader.cs b/source/Cosmos.IL2CPU/ILReader.cs index 4cbaeecd7..0d414079b 100644 --- a/source/Cosmos.IL2CPU/ILReader.cs +++ b/source/Cosmos.IL2CPU/ILReader.cs @@ -6,10 +6,11 @@ using System.Reflection.Metadata; using Cosmos.IL2CPU.Extensions; +using Cosmos.IL2CPU.Optimization; namespace Cosmos.IL2CPU { - public class ILReader + public class ILReader : IOptimizerLookaheadProvider { // We split this into two arrays since we have to read // a byte at a time anways. In the future if we need to @@ -19,6 +20,14 @@ public class ILReader private static readonly OpCode[] mOpCodesLo = new OpCode[256]; private static readonly OpCode[] mOpCodesHi = new OpCode[256]; + private static readonly Dictionary> methodCache = new(); + private static readonly Dictionary> lookaheadCache = new(); + + /// + /// The optimizer to use. + /// + public Optimizer Optimizer { get; set; } + public ILReader() { LoadOpCodes(); @@ -50,10 +59,22 @@ protected void CheckBranch(int aTarget, int aMethodSize) } } - public List ProcessMethod(MethodBase aMethod) + public List ProcessMethodAhead(MethodBase aMethod) { - var xResult = new List(); + return ProcessMethod(aMethod, lookahead: true); + } + + public List ProcessMethod(MethodBase aMethod, bool lookahead = false) + { + if (methodCache.TryGetValue(aMethod, out var result)) { + return result; + } + if(lookahead && lookaheadCache.TryGetValue(aMethod, out var lookaheadResult)) { + return lookaheadResult; + } + + var xResult = new List(); var xBody = aMethod.GetMethodBody(); var xModule = aMethod.Module; @@ -128,7 +149,6 @@ public List ProcessMethod(MethodBase aMethod) #endregion - #region ByReference Intrinsic if (aMethod.DeclaringType.IsGenericType @@ -657,10 +677,27 @@ public List ProcessMethod(MethodBase aMethod) default: throw new Exception("Unknown OperandType"); } - xILOpCode.InitStackAnalysis(aMethod); + + //xILOpCode.InitStackAnalysis(aMethod); xResult.Add(xILOpCode); } + if (!lookahead) { + xResult = Optimizer.Optimize(xResult); + } + + foreach (var opCode in xResult) { + opCode.InitStackAnalysis(aMethod); + } + + if (lookahead) { + lookaheadCache[aMethod] = xResult; + } + else { + methodCache[aMethod] = xResult; + lookaheadCache.Remove(aMethod); + } + return xResult; } diff --git a/source/Cosmos.IL2CPU/ILScanner.cs b/source/Cosmos.IL2CPU/ILScanner.cs index f714bbe05..1db465839 100644 --- a/source/Cosmos.IL2CPU/ILScanner.cs +++ b/source/Cosmos.IL2CPU/ILScanner.cs @@ -11,6 +11,8 @@ using IL2CPU.API; using IL2CPU.API.Attribs; using XSharp.Assembler; +using Cosmos.IL2CPU.Optimization; +using Cosmos.IL2CPU.Optimization.Passes; namespace Cosmos.IL2CPU { @@ -82,6 +84,11 @@ public ILScanner(AppAssembler aAsmblr, TypeResolver typeResolver, Action + /// Represents an object that can provide methods that can be used in + /// look-ahead operations by an . + /// + /// + /// A look-ahead is used when an requires to + /// compute the method body of a method it hasn't been provided with before. + /// + public interface IOptimizerLookaheadProvider + { + /// + /// Provides the method body for the given method, using a look-ahead lookup. + /// When using a optimization look-ahead, no optimizations are applied, and + /// a different cache is used. + /// + /// The method to compute the body of. + public List ProcessMethodAhead(MethodBase aMethod); + } +} diff --git a/source/Cosmos.IL2CPU/Optimization/Optimizer.cs b/source/Cosmos.IL2CPU/Optimization/Optimizer.cs new file mode 100644 index 000000000..a059ffa4f --- /dev/null +++ b/source/Cosmos.IL2CPU/Optimization/Optimizer.cs @@ -0,0 +1,74 @@ +using System; +using System.Linq; +using System.Collections.Generic; + +namespace Cosmos.IL2CPU.Optimization +{ + /// + /// Optimizes IL methods. + /// + public class Optimizer + { + /// + /// The method look-ahead provider that can be used to compute the method body of a method it hasn't been provided with before. + /// + public IOptimizerLookaheadProvider LookaheadProvider { get; set; } + + /// + /// The passes that the optimizer will use. + /// + public List Passes { get; init; } = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The look-ahead provider to use. + public Optimizer(IOptimizerLookaheadProvider aLookaheadProvider) + { + LookaheadProvider = aLookaheadProvider ?? throw new ArgumentNullException(nameof(aLookaheadProvider)); + } + + /// + /// Exclude the given type of optimizer passes in this optimizer. + /// + /// The type to exclude. + /// Thrown when the given type is not a subclass of . + public void ExcludePassType(Type passType) + { + if(!passType.IsSubclassOf(typeof(OptimizerPass))) { + throw new InvalidOperationException("The given type is not a subclass of OptimizerPass."); + } + + for (int i = Passes.Count - 1; i >= 0; i--) { + if (Passes[i].GetType() == passType) { + Passes.RemoveAt(i); + } + } + } + + /// + /// Optimizes the given method. + /// + /// The body of the method, in IL instructions. + /// The optimized method body. + public List Optimize(List instructions) + { + foreach (var pass in Passes) { + instructions = pass.Process(instructions); + } + + return instructions; + } + + /// + /// Adds a pass to this and returns the object. + /// + /// The pass to add to the optimizer. + public Optimizer WithPass(OptimizerPass aPass) + { + aPass.Owner = this; + Passes.Add(aPass); + return this; + } + } +} diff --git a/source/Cosmos.IL2CPU/Optimization/OptimizerPass.cs b/source/Cosmos.IL2CPU/Optimization/OptimizerPass.cs new file mode 100644 index 000000000..ff3146531 --- /dev/null +++ b/source/Cosmos.IL2CPU/Optimization/OptimizerPass.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Cosmos.IL2CPU.Optimization +{ + /// + /// Represents a single optimization pass. + /// + public abstract class OptimizerPass + { + /// + /// The owner of this . + /// + public Optimizer Owner { get; internal set; } + + /// + /// Optimizes the given method. + /// + /// The body of the target method. + /// The optimized method body. + public abstract List Process(List instructions); + } +} diff --git a/source/Cosmos.IL2CPU/Optimization/Passes/InlineDirectPropertiesPass.cs b/source/Cosmos.IL2CPU/Optimization/Passes/InlineDirectPropertiesPass.cs new file mode 100644 index 000000000..b8c5615ec --- /dev/null +++ b/source/Cosmos.IL2CPU/Optimization/Passes/InlineDirectPropertiesPass.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Cosmos.IL2CPU.ILOpCodes; + +namespace Cosmos.IL2CPU.Optimization.Passes +{ + /// + /// Inlines direct property accesses. A direct property access is a call + /// to a getter/setter method that directly gets or sets an underlying field, + /// which can be simplified to a single instruction. + /// + internal class InlineDirectPropertiesPass : OptimizerPass + { + // Control value to cancel optimization by look-ahead provider + bool processing = false; + + public override List Process(List il) + { + if (processing) { + return il; + } + + processing = true; + + var callsToInline = Enumerable.Range(0, il.Count) + // Select both the index and the instruction under that index. + .Select(i => (idx: i, inst: il[i])) + // Filter to only call instructions from which we can get a MethodBase. + .Where(x => x.inst.OpCode == ILOpCode.Code.Call && x.inst is OpMethod) + // Transform to (int, OpMethod). + .Select(x => (x.idx, inst: (OpMethod)x.inst)) + // Ensure that inst.Value is not null. + .Where(x => x.inst.Value != null) + // Apply a look-ahead to see what method this instruction wants to call. + .Select(x => (x.idx, x.inst, body: Owner.LookaheadProvider.ProcessMethodAhead(x.inst.Value))) + // Some methods return null bodies; filter them out. + .Where(x => x.body != null) + // Find the inline instruction replacements for the method calls, now that we have the body of the method its trying to call. + .Select(x => (x.idx, x.inst, x.body, inline: GetPropertyInlineReplacement(x.inst.Value, x.body))) + // If there is no inline replacement, don't include it in the list. + .Where(x => x.inline != null); + + foreach (var x in callsToInline) { + // Create a new OpField that fits into our sequence + // OpField and OpMethod should have the same length, so we can just replace Position and NextPosition + // with the OpMethod instructions we are replacing + var inline = new OpField( + x.inline.OpCode, + il[x.idx].Position, il[x.idx].NextPosition, + x.inline.Value, + x.inline.CurrentExceptionRegion + ); + + il[x.idx] = inline; + } + + processing = false; + return il; + } + + + /// + /// Determines whether the provided method body is a direct property accessor + /// and returns the field target instruction. + /// + private static OpField GetPropertyInlineReplacement(MethodBase info, List il) + { + var filtered = il.Where(x => x.OpCode != ILOpCode.Code.Nop) + .ToArray(); + + if (info is not MethodInfo moreInfo) { + // The method information isn't MethodInfo... this shouldn't happen! + return null; + } + + bool setter = false; + if(moreInfo.ReturnType == typeof(void) && info.GetParameters().Length == 1) { + setter = true; + } else if(moreInfo.GetParameters().Length != 0){ + // Not a setter, and accepts parameters; not a property + return null; + } + + if (info.IsStatic) { + if(setter) { + var isDirectAccess = + filtered.Length >= 3 && + filtered[0].OpCode == ILOpCode.Code.Ldarg && ((OpVar)filtered[0]).Value == 0 && + filtered[1].OpCode == ILOpCode.Code.Stsfld && + filtered[2].OpCode == ILOpCode.Code.Ret; + + if(isDirectAccess) { + // The property loads argument 0 (value), that is in turn taken from the stack. + // The field then is changed with the value on the stack and returns; replace the call + // with the stsfld instruction (that sets the field). + return (OpField)filtered[1]; + } + } else { + var isDirectAccess = + filtered.Length >= 2 && + filtered[0].OpCode == ILOpCode.Code.Ldsfld && + filtered[1].OpCode == ILOpCode.Code.Ret; + + if (isDirectAccess) { + // The property pushes the field on the stack and returns; replace the call + // with the ldsfld instruction (that pushes the field). + return (OpField)filtered[0]; + } + } + } + else { + if(setter) { + var isDirectAccess = + filtered.Length >= 4 && + filtered[0].OpCode == ILOpCode.Code.Ldarg && ((OpVar)filtered[0]).Value == 0 && + filtered[1].OpCode == ILOpCode.Code.Ldarg && ((OpVar)filtered[1]).Value == 1 && + filtered[2].OpCode == ILOpCode.Code.Stfld && + filtered[3].OpCode == ILOpCode.Code.Ret; + + if(isDirectAccess) { + // The property pushes two arguments onto the stack; the instance pointer + // and the value, and calls stfld. In order to call this method, the caller + // must also push the target instance pointer (even if the caller is the object + // that the property belongs to; in that case, it would do "ldarg.0") and the + // value. The stack layout does not change, meaning we can directly replace the call. + return (OpField)filtered[2]; + } + } else { + var isDirectAccess = + filtered.Length >= 3 && + filtered[0].OpCode == ILOpCode.Code.Ldarg && ((OpVar)filtered[0]).Value == 0 && + filtered[1].OpCode == ILOpCode.Code.Ldfld && + filtered[2].OpCode == ILOpCode.Code.Ret; + + if(isDirectAccess) { + // The property loads argument 0 (self pointer), executes the instruction + // ldfld, and returns. In order to call the property, we already push argument 0, + // meaning we can direclty replace the call. + return (OpField)filtered[1]; + } + } + } + + // No instruction that can be used to directly replace the call + return null; + } + } +} diff --git a/source/Cosmos.IL2CPU/Optimizer.cs b/source/Cosmos.IL2CPU/Optimizer.cs deleted file mode 100644 index ad50ce641..000000000 --- a/source/Cosmos.IL2CPU/Optimizer.cs +++ /dev/null @@ -1,73 +0,0 @@ -using XSharp.Assembler; - -namespace Cosmos.IL2CPU -{ - internal static class Optimizer - { - public static Assembler Optimize(Assembler asmb) - { - return asmb; - //Assembler asmblr = asmb; - //List instr = asmb.Instructions; - ////List dmbrs = asmb.DataMembers; - - //SortedDictionary labels = new SortedDictionary(); - //List comments = new List(); - //List usedLabels = new List(); - //usedLabels.Add("KernelStart"); - //foreach (Instruction ins in instr) { - // if (ins is Label) { - // if (((Label)ins).IsGlobal) { - // usedLabels.Add(((Label)ins).QualifiedName); - // } - // labels.Add(((Label)ins).QualifiedName, ins); - // } else if (ins is x86.JumpToSegment) { - // if (((x86.JumpToSegment)ins).DestinationRef != null) { - // usedLabels.Add(((x86.JumpToSegment)ins).DestinationRef.Name); - // } else { - // usedLabels.Add(((x86.JumpToSegment)ins).DestinationLabel); - // } - // } else if (ins is x86.JumpBase) { - // usedLabels.Add(((x86.JumpBase)ins).DestinationLabel); - // } else if (ins is x86.Call) { - // usedLabels.Add(((x86.Call)ins).DestinationLabel); - // } else if (ins is x86.Push) { - // if (((x86.Push)ins).DestinationRef != null) { - // usedLabels.Add(((x86.Push)ins).DestinationRef.Name); - // } - // } else if (ins is x86.Mov) { - // if (((x86.Mov)ins).SourceRef != null) { - // usedLabels.Add(((x86.Mov)ins).SourceRef.Name); - // } - // } - //} - //foreach (string s in usedLabels) { - // labels.Remove(s); - //} - //usedLabels = null; - //instr.RemoveAll( - // delegate(Instruction inst) { - // if (inst is Comment) - // return true; - // else if (inst is Label) { - // if (labels.ContainsKey(((Label)inst).QualifiedName)) - // return true; - // return false; - // } - // return false; - // } - //); - //labels = null; - //comments = null; - - - - - //asmblr.Instructions = instr; - ////asmblr.DataMembers = dmbrs; - //return asmblr; - - } - - } -} From 0eb4c531ef352d5778702cad26463d5acabd6d88 Mon Sep 17 00:00:00 2001 From: ascpixi <44982772+ascpixi@users.noreply.github.com> Date: Sat, 29 Oct 2022 20:31:59 +0200 Subject: [PATCH 2/3] Document ExceptionRegioninfo --- source/Cosmos.IL2CPU/_ExceptionRegionInfo.cs | 85 ++++++++++++++++---- 1 file changed, 68 insertions(+), 17 deletions(-) diff --git a/source/Cosmos.IL2CPU/_ExceptionRegionInfo.cs b/source/Cosmos.IL2CPU/_ExceptionRegionInfo.cs index 296009cee..6aa8eda75 100644 --- a/source/Cosmos.IL2CPU/_ExceptionRegionInfo.cs +++ b/source/Cosmos.IL2CPU/_ExceptionRegionInfo.cs @@ -4,49 +4,100 @@ namespace Cosmos.IL2CPU { + /// + /// Represents the information about a clause in a structured exception-handling block. + /// public class _ExceptionRegionInfo { + /// + /// The exception handling clause that this object describes. + /// public ExceptionHandlingClause ExceptionClause { get; } + + /// + /// Gets the offset within the method body, in bytes, of this exception-handling clause. + /// public int HandlerOffset { get; } + + /// + /// Gets the length, in bytes, of the body of this exception-handling clause. + /// public int HandlerLength { get; } + + /// + /// The offset within the method, in bytes, of the try block that includes this exception-handling clause. + /// public int TryOffset { get; } + + /// + /// The total length, in bytes, of the try block that includes this exception-handling clause. + /// public int TryLength { get; } + + /// + /// Gets the offset within the method body, in bytes, of the user-supplied filter code. + /// public int FilterOffset { get; } + + /// + /// Gets the type of this exception region. + /// public ExceptionRegionKind Kind { get; } + + /// + /// Gets the type of exception handled by this clause. + /// public Type CatchType { get; } + /// + /// Creates a new instance of the class, describing + /// the given exception handling clause. + /// + /// The exception handling clause that the newly created object should describe. public _ExceptionRegionInfo(ExceptionHandlingClause aExceptionClause) + : this( + aExceptionClause, + aExceptionClause.HandlerOffset, + aExceptionClause.HandlerLength, + aExceptionClause.TryOffset, + aExceptionClause.TryLength, + aExceptionClause.Flags == ExceptionHandlingClauseOptions.Filter ? aExceptionClause.FilterOffset : 0 + ) + { + + } + + /// + /// Creates a new instance of the class, describing + /// the given exception handling clause with the given region offsets and lengths. + /// + /// The exception handling clause that the newly created object should describe. + internal _ExceptionRegionInfo(ExceptionHandlingClause aExceptionClause, int aHandlerOffset, int aHandlerLength, int aTryOffset, int aTryLength, int aFilterOffset) { - try - { + try { ExceptionClause = aExceptionClause; - HandlerOffset = aExceptionClause.HandlerOffset; - HandlerLength = aExceptionClause.HandlerLength; - TryOffset = aExceptionClause.TryOffset; - TryLength = aExceptionClause.TryLength; + HandlerOffset = aHandlerOffset; + HandlerLength = aHandlerLength; + TryOffset = aTryOffset; + TryLength = aTryLength; - if (aExceptionClause.Flags == ExceptionHandlingClauseOptions.Clause) - { + if (aExceptionClause.Flags == ExceptionHandlingClauseOptions.Clause) { Kind = ExceptionRegionKind.Catch; CatchType = aExceptionClause.CatchType; } - else if (aExceptionClause.Flags.HasFlag(ExceptionHandlingClauseOptions.Fault)) - { + else if (aExceptionClause.Flags.HasFlag(ExceptionHandlingClauseOptions.Fault)) { Kind = ExceptionRegionKind.Fault; } - else if (aExceptionClause.Flags.HasFlag(ExceptionHandlingClauseOptions.Filter)) - { + else if (aExceptionClause.Flags.HasFlag(ExceptionHandlingClauseOptions.Filter)) { Kind = ExceptionRegionKind.Filter; - FilterOffset = aExceptionClause.FilterOffset; + FilterOffset = aFilterOffset; CatchType = typeof(System.Exception); //TODO: Confirm that this is correct. } - else if (aExceptionClause.Flags.HasFlag(ExceptionHandlingClauseOptions.Finally)) - { + else if (aExceptionClause.Flags.HasFlag(ExceptionHandlingClauseOptions.Finally)) { Kind = ExceptionRegionKind.Finally; } } - catch - { + catch { // ignored } } From a50cc6de15b3763e09b3e9bce9b50f3b079fea9e Mon Sep 17 00:00:00 2001 From: valentinbreiz Date: Sun, 24 Dec 2023 04:24:30 +0100 Subject: [PATCH 3/3] Init optimizer --- source/Cosmos.IL2CPU/ILScanner.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/source/Cosmos.IL2CPU/ILScanner.cs b/source/Cosmos.IL2CPU/ILScanner.cs index 043c39307..235c09c30 100644 --- a/source/Cosmos.IL2CPU/ILScanner.cs +++ b/source/Cosmos.IL2CPU/ILScanner.cs @@ -10,6 +10,9 @@ using IL2CPU.API.Attribs; using XSharp.Assembler; +using Cosmos.IL2CPU.Optimization; +using Cosmos.IL2CPU.Optimization.Passes; + namespace Cosmos.IL2CPU { public class ScannerQueueItem @@ -80,6 +83,11 @@ public ILScanner(AppAssembler aAsmblr, TypeResolver typeResolver, Action