From 319cff734dc9e42ece8087f71dc9279d8403943e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20R=C3=A4tzel?= Date: Wed, 11 Sep 2024 17:53:59 +0000 Subject: [PATCH] Improve runtime efficiency by caching common derivations of expressions To reduce computational expenses for frequently used aggregate properties derived from expressions and their descendant trees, store these derivations directly in each instance's representation in memory. --- implement/Pine.Core/PineVM/Expression.cs | 251 ++++++++++++++---- implement/Pine.Core/ReusedInstances.cs | 21 +- .../Pine.UnitTests/ExpressionTests.cs | 127 +++++++++ .../CompilePineToDotNet/CompileToCSharp.cs | 2 +- .../PineKernelFunctions.cs | 2 +- .../ReducePineExpression.cs | 35 ++- implement/pine/Pine/PineVM/PineVM.cs | 13 +- 7 files changed, 364 insertions(+), 87 deletions(-) create mode 100644 implement/PineTest/Pine.UnitTests/ExpressionTests.cs diff --git a/implement/Pine.Core/PineVM/Expression.cs b/implement/Pine.Core/PineVM/Expression.cs index 66016141..59e8ae79 100644 --- a/implement/Pine.Core/PineVM/Expression.cs +++ b/implement/Pine.Core/PineVM/Expression.cs @@ -18,6 +18,13 @@ namespace Pine.PineVM; [JsonConverter(typeof(JsonConverterForChoiceType))] public abstract record Expression { + public abstract int SubexpressionCount { get; } + + /// + /// True if the expression itself or any its subexpressions is of type . + /// + public abstract bool ReferencesEnvironment { get; } + public static readonly Expression EnvironmentInstance = new Environment(); public static readonly Literal LiteralEmptyListInstance = @@ -76,13 +83,22 @@ public static Conditional ConditionalInstance( public record Literal( PineValue Value) - : Expression; + : Expression + { + public override int SubexpressionCount { get; } = 0; + + public override bool ReferencesEnvironment { get; } = false; + } public record List : Expression { private readonly int slimHashCode; + public override int SubexpressionCount { get; } + + public override bool ReferencesEnvironment { get; } = false; + public IReadOnlyList items { get; } internal List(IReadOnlyList items) @@ -96,6 +112,16 @@ internal List(ListStruct listKey) items = listKey.Items; slimHashCode = listKey.slimHashCode; + + SubexpressionCount = items.Count; + + for (int i = 0; i < items.Count; ++i) + { + SubexpressionCount += items[i].SubexpressionCount; + + if (items[i].ReferencesEnvironment) + ReferencesEnvironment = true; + } } public virtual bool Equals(List? other) @@ -156,16 +182,54 @@ public static int ComputeHashCode(IReadOnlyList items) } } - public record ParseAndEval( - Expression encoded, - Expression environment) - : Expression; + public record ParseAndEval + : Expression + { + public Expression encoded { get; } + + public Expression environment { get; } + + public override int SubexpressionCount { get; } + + public override bool ReferencesEnvironment { get; } + + public ParseAndEval( + Expression encoded, + Expression environment) + { + this.encoded = encoded; + this.environment = environment; + + SubexpressionCount = + encoded.SubexpressionCount + environment.SubexpressionCount + 2; + + ReferencesEnvironment = + encoded.ReferencesEnvironment || environment.ReferencesEnvironment; + } + } - public record KernelApplication( - string function, - Expression input) + public record KernelApplication : Expression { + public string function { get; } + + public Expression input { get; } + + public override int SubexpressionCount { get; } + + public override bool ReferencesEnvironment { get; } + + public KernelApplication( + string function, + Expression input) + { + this.function = function; + this.input = input; + + SubexpressionCount = input.SubexpressionCount + 1; + ReferencesEnvironment = input.ReferencesEnvironment; + } + public virtual bool Equals(KernelApplication? other) { if (other is not { } notNull) @@ -198,6 +262,10 @@ public record Conditional public Expression trueBranch { get; } + public override int SubexpressionCount { get; } + + public override bool ReferencesEnvironment { get; } + internal Conditional( Expression condition, Expression falseBranch, @@ -215,6 +283,17 @@ internal Conditional( trueBranch = conditionalStruct.TrueBranch; slimHashCode = conditionalStruct.slimHashCode; + + SubexpressionCount = + condition.SubexpressionCount + + falseBranch.SubexpressionCount + + trueBranch.SubexpressionCount + + 3; + + ReferencesEnvironment = + condition.ReferencesEnvironment || + falseBranch.ReferencesEnvironment || + trueBranch.ReferencesEnvironment; } public virtual bool Equals(Conditional? other) @@ -279,15 +358,64 @@ public static int ComputeHashCode( } } - public record Environment : Expression; + public record Environment : Expression + { + public override int SubexpressionCount { get; } = 0; + + public override bool ReferencesEnvironment { get; } = true; + } + + public record StringTag + : Expression + { + public string tag { get; } + + public Expression tagged { get; } - public record StringTag( - string tag, - Expression tagged) - : Expression; + public override int SubexpressionCount { get; } + + public override bool ReferencesEnvironment { get; } + + public readonly int slimHashCode; + + public StringTag( + string tag, + Expression tagged) + { + this.tag = tag; + this.tagged = tagged; + + SubexpressionCount = tagged.SubexpressionCount + 1; + ReferencesEnvironment = tagged.ReferencesEnvironment; + + slimHashCode = HashCode.Combine(tag, tagged); + } + + public override int GetHashCode() => + slimHashCode; + + public virtual bool Equals(StringTag? other) + { + if (ReferenceEquals(other, this)) + return true; + + if (other is null) + return false; + + return + other.slimHashCode == slimHashCode && + other.tag == tag && + other.tagged == tagged; + } + } public record StackReferenceExpression(int offset) - : Expression; + : Expression + { + public override int SubexpressionCount { get; } = 0; + + public override bool ReferencesEnvironment { get; } = false; + } /// /// Fusion of the two applications of the kernel functions 'skip' and 'head', @@ -296,8 +424,14 @@ public record StackReferenceExpression(int offset) public record KernelApplications_Skip_Head_Path( ReadOnlyMemory SkipCounts, Expression Argument) - : Expression + : Expression { + public override int SubexpressionCount { get; } = + Argument.SubexpressionCount + 1; + + public override bool ReferencesEnvironment { get; } = + Argument.ReferencesEnvironment; + public virtual bool Equals(KernelApplications_Skip_Head_Path? other) { if (other is null) @@ -328,47 +462,73 @@ public override int GetHashCode() public record KernelApplication_Equal_Two( Expression left, Expression right) - : Expression; + : Expression + { + public override int SubexpressionCount { get; } = + left.SubexpressionCount + right.SubexpressionCount + 2; - /// - /// Returns true if the expression is independent of the environment, that is none of the expressions in the tree contain an . - /// - public static bool IsIndependent(Expression expression) => - expression switch + public override bool ReferencesEnvironment { get; } = + left.ReferencesEnvironment || right.ReferencesEnvironment; + } + + public static IReadOnlySet CollectAllComponentsFromRoots( + IEnumerable roots) + { + var components = new HashSet(); + + var stack = new Stack(roots); + + while (stack.TryPop(out var expression)) { - Environment => - false, + if (components.Contains(expression)) + continue; + + components.Add(expression); - Literal => - true, + IReadOnlyList childItems = + expression switch + { + List listExpression => + listExpression.items, - List list => - list.items.All(IsIndependent), + Conditional conditionalExpression => + [ + conditionalExpression.condition, + conditionalExpression.falseBranch, + conditionalExpression.trueBranch + ], - ParseAndEval decodeAndEvaluate => - IsIndependent(decodeAndEvaluate.encoded) && IsIndependent(decodeAndEvaluate.environment), + KernelApplication kernelApplicationExpression => + [kernelApplicationExpression.input], - KernelApplication kernelApplication => - IsIndependent(kernelApplication.input), + ParseAndEval parseAndEval => + [parseAndEval.environment, parseAndEval.encoded], - Conditional conditional => - IsIndependent(conditional.condition) && IsIndependent(conditional.trueBranch) && IsIndependent(conditional.falseBranch), + StringTag stringTagExpression => + [stringTagExpression.tagged], - StringTag stringTag => - IsIndependent(stringTag.tagged), + Literal => + [], - StackReferenceExpression => - false, + Environment => + [], - KernelApplications_Skip_Head_Path fused => - IsIndependent(fused.Argument), + StackReferenceExpression => + [], - KernelApplication_Equal_Two fused => - IsIndependent(fused.left) && IsIndependent(fused.right), + _ => + throw new NotImplementedException( + "Unexpected expression type: " + expression.GetType()) + }; - _ => - throw new NotImplementedException(), - }; + foreach (var item in childItems) + { + stack.Push(item); + } + } + + return components; + } public static IEnumerable EnumerateSelfAndDescendants( Expression expression) => @@ -444,7 +604,8 @@ public static IEnumerable EnumerateSelfAndDescendants( break; default: - throw new NotImplementedException("Unknown expression type: " + expression.GetType().FullName); + throw new NotImplementedException( + "Unknown expression type: " + expression.GetType().FullName); } } } diff --git a/implement/Pine.Core/ReusedInstances.cs b/implement/Pine.Core/ReusedInstances.cs index 1153d1c3..863537d1 100644 --- a/implement/Pine.Core/ReusedInstances.cs +++ b/implement/Pine.Core/ReusedInstances.cs @@ -10,8 +10,7 @@ public record ReusedInstances( IEnumerable expressionRootsSource) { public static readonly ReusedInstances Instance = - new( - expressionRootsSource: ExpressionsSource()); + new(expressionRootsSource: ExpressionsSource()); public FrozenDictionary? ListValues { private set; get; } @@ -155,24 +154,12 @@ listItem is PineValue.ListValue oldItemAsList var expressionsRoots = expressionRootsSource.ToList(); - - var allExpressionDescendants = new HashSet(); - - /* - * TODO: Optimize collecting all components and sorting by size. - * */ - - foreach (var expressionRoot in expressionsRoots) - { - foreach (var descendant in Expression.EnumerateSelfAndDescendants(expressionRoot)) - { - allExpressionDescendants.Add(descendant); - } - } + var allExpressionDescendants = + Expression.CollectAllComponentsFromRoots(expressionsRoots); var allDescendantsOrdered = allExpressionDescendants - .OrderBy(expression => Expression.EnumerateSelfAndDescendants(expression).Count()) + .OrderBy(expression => expression.SubexpressionCount) .ToList(); var reusedInstancesInConstruction = new Dictionary(); diff --git a/implement/PineTest/Pine.UnitTests/ExpressionTests.cs b/implement/PineTest/Pine.UnitTests/ExpressionTests.cs new file mode 100644 index 00000000..f85fd3fd --- /dev/null +++ b/implement/PineTest/Pine.UnitTests/ExpressionTests.cs @@ -0,0 +1,127 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Pine.PineVM; +using System.Collections.Generic; +using System.Linq; + +namespace Pine.UnitTests; + +[TestClass] +public class ExpressionTests +{ + [TestMethod] + public void Expression_aggregate_properties_equal_derived_from_generic_enumeration() + { + IReadOnlyList testCases = + [ + Expression.EnvironmentInstance, + + Expression.LiteralInstance(PineValue.EmptyList), + + Expression.ListInstance([]), + Expression.ListInstance([Expression.EnvironmentInstance]), + Expression.ListInstance( + [ + Expression.EnvironmentInstance, + Expression.LiteralInstance(PineValue.EmptyList) + ]), + + Expression.ListInstance( + [ + Expression.EnvironmentInstance, + Expression.ListInstance( + [ + Expression.EnvironmentInstance, + Expression.LiteralInstance(PineValue.EmptyList), + ]), + Expression.ListInstance([]), + ]), + + Expression.ConditionalInstance( + Expression.ListInstance([]), + Expression.ListInstance( + [ + Expression.EnvironmentInstance, + Expression.LiteralInstance(PineValue.EmptyList), + ]), + Expression.ListInstance([])), + + Expression.ConditionalInstance( + Expression.ListInstance([]), + Expression.ListInstance( + [ + Expression.LiteralInstance(PineValue.EmptyBlob), + Expression.LiteralInstance(PineValue.EmptyList), + ]), + Expression.ListInstance([])), + + new Expression.ParseAndEval( + Expression.EnvironmentInstance, + Expression.EnvironmentInstance), + + new Expression.KernelApplication( + "function", + Expression.EnvironmentInstance), + + new Expression.StringTag( + "test", + Expression.ListInstance([])), + + new Expression.StringTag( + "test", + Expression.EnvironmentInstance), + + new Expression.StringTag( + "test", + Expression.ListInstance([Expression.EnvironmentInstance])), + ]; + + foreach (var testCase in testCases) + { + var rootAndSubexpressions = + Expression.EnumerateSelfAndDescendants(testCase) + .ToList(); + + var subexpressions = + rootAndSubexpressions + .Skip(1) + .ToList(); + + var anyNodeIsEnvironment = + rootAndSubexpressions.OfType().Any(); + + Assert.AreEqual( + subexpressions.Count, + testCase.SubexpressionCount); + + Assert.AreEqual( + anyNodeIsEnvironment, + testCase.ReferencesEnvironment); + } + } + + /* + [TestMethod] + public void With_expression_works_with_conditional_expression() + { + var originalExpression = + Expression.ConditionalInstance( + condition: + new Expression.KernelApplication( + function: "equal", + input: Expression.EnvironmentInstance), + falseBranch: + Expression.LiteralInstance(PineValue.EmptyList), + trueBranch: + Expression.LiteralInstance(PineValue.EmptyBlob)); + + var afterWithOperation = + originalExpression + with + { + condition = Expression.LiteralInstance(PineValue.EmptyBlob) + }; + + Assert.IsTrue(originalExpression.ReferencesEnvironment); + } + */ +} diff --git a/implement/pine/Pine/CompilePineToDotNet/CompileToCSharp.cs b/implement/pine/Pine/CompilePineToDotNet/CompileToCSharp.cs index 66570591..d48f6d04 100644 --- a/implement/pine/Pine/CompilePineToDotNet/CompileToCSharp.cs +++ b/implement/pine/Pine/CompilePineToDotNet/CompileToCSharp.cs @@ -1251,7 +1251,7 @@ bool ChildEnvContstraintItemSatisfied(KeyValuePair, PineValue }); } - if (Expression.IsIndependent(parseAndEvalExpr.encoded)) + if (!parseAndEvalExpr.encoded.ReferencesEnvironment) { return ReducePineExpression.TryEvaluateExpressionIndependent(parseAndEvalExpr.encoded) diff --git a/implement/pine/Pine/CompilePineToDotNet/PineKernelFunctions.cs b/implement/pine/Pine/CompilePineToDotNet/PineKernelFunctions.cs index b6511aca..274d552b 100644 --- a/implement/pine/Pine/CompilePineToDotNet/PineKernelFunctions.cs +++ b/implement/pine/Pine/CompilePineToDotNet/PineKernelFunctions.cs @@ -56,7 +56,7 @@ Result> continu { PineValue.ListValue literalList => continueWithList( - literalList.Elements.Select(elementValue => Expression.LiteralInstance(elementValue))), + literalList.Elements.Select(Expression.LiteralInstance)), _ => null }, diff --git a/implement/pine/Pine/CompilePineToDotNet/ReducePineExpression.cs b/implement/pine/Pine/CompilePineToDotNet/ReducePineExpression.cs index 2c0727dc..00cac8d9 100644 --- a/implement/pine/Pine/CompilePineToDotNet/ReducePineExpression.cs +++ b/implement/pine/Pine/CompilePineToDotNet/ReducePineExpression.cs @@ -140,7 +140,7 @@ public static Result TryEvaluateExpressionIndependent( Expression? AttemptReduceViaEval() { - if (Expression.IsIndependent(expression)) + if (!expression.ReferencesEnvironment) { try { @@ -148,7 +148,7 @@ public static Result TryEvaluateExpressionIndependent( TryEvaluateExpressionIndependent(expression) .Unpack( fromErr: _ => null, - fromOk: literalValue => Expression.LiteralInstance(literalValue)); + fromOk: Expression.LiteralInstance); } catch (ParseExpressionException) { @@ -166,6 +166,12 @@ public static Result TryEvaluateExpressionIndependent( switch (expression) { case Expression.KernelApplication rootKernelApp: + + Expression.KernelApplication continueWithReducedInput(Expression newInput) => + new( + function: rootKernelApp.function, + input: newInput); + switch (rootKernelApp.function) { case nameof(KernelFunction.equal): @@ -286,17 +292,14 @@ listLengthLowerBounds.Count is 0 ? var aggregateSkipCount = outerSkipCountClamped + innerSkipCountClamped; return - rootKernelApp - with - { - input = Expression.ListInstance( + continueWithReducedInput( + Expression.ListInstance( [ Expression.LiteralInstance( PineValueAsInteger.ValueFromSignedInteger(aggregateSkipCount)), innerSkipInputList.items[1] ] - ) - }; + )); } } } @@ -362,11 +365,7 @@ listLengthLowerBounds.Count is 0 ? } return - rootKernelApp - with - { - input = Expression.ListInstance(nonEmptyItems) - }; + continueWithReducedInput(Expression.ListInstance(nonEmptyItems)); } } @@ -464,7 +463,7 @@ listLengthLowerBounds.Count is 0 ? conditional.condition, envConstraintId: envConstraintId); - if (Expression.IsIndependent(condition)) + if (!condition.ReferencesEnvironment) { return TryEvaluateExpressionIndependent(condition) @@ -628,11 +627,9 @@ public static (Expression expr, bool referencesOriginalEnv) TransformPineExpress return ( - kernelApp - with - { - input = argumentTransform.expr - }, + new Expression.KernelApplication( + function: kernelApp.function, + input: argumentTransform.expr), argumentTransform.referencesOriginalEnv); } diff --git a/implement/pine/Pine/PineVM/PineVM.cs b/implement/pine/Pine/PineVM/PineVM.cs index b95cc993..4beb0ec3 100644 --- a/implement/pine/Pine/PineVM/PineVM.cs +++ b/implement/pine/Pine/PineVM/PineVM.cs @@ -597,7 +597,7 @@ envConstraintId is null return inlinedFinal; } - if (Expression.IsIndependent(parseAndEvalExpr.encoded)) + if (!parseAndEvalExpr.encoded.ReferencesEnvironment) { if (CompilePineToDotNet.ReducePineExpression.TryEvaluateExpressionIndependent(parseAndEvalExpr.encoded) is Result.Ok evalExprOk) @@ -1201,11 +1201,14 @@ static bool ExpressionLargeEnoughForCSE(Expression expression) if (ExpressionLargeEnoughForCSE(list.items[i])) return true; } + } - return false; + if (expression is Expression.StringTag stringTag) + { + return ExpressionLargeEnoughForCSE(stringTag.tagged); } - return false; + return 10 < expression.SubexpressionCount; } public static Expression? TryFuseStep(Expression expression) @@ -1253,9 +1256,11 @@ public static Expression.KernelApplications_Skip_Head_Path? return null; */ + if (skipCountValueExpr.ReferencesEnvironment) + return null; + if (Expression.EnumerateSelfAndDescendants(skipCountValueExpr) .Any(desc => - desc is Expression.Environment || desc is Expression.ParseAndEval || desc is Expression.StackReferenceExpression)) {