Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid lambda compilation as much as possible #2957

Merged
merged 16 commits into from
Dec 14, 2021

Conversation

gokhanabatay
Copy link
Contributor

@gokhanabatay gokhanabatay commented Dec 4, 2021

All linq dml style(update/delete) and select queries take times for compilation issue, with Dynatrace monitoring we can see elapsed time(self time) in ExpressionProcessor is too much depending of your query.

When I open #2952 I tell about suspensions because of expression compile every time causes gc pressure.
For performance point of view I think we need to avoid expression compile as much as possible.
For queries those are not cacheable(query plan cache) we need to create warning log, because this kind of performance issue is not easy to find out.

Below image shows single query calls 6 times ExpressionProcessor.FindValue, just 2 of them calls
Expression.Lambda(expression).Compile().DynamicInvoke() and takes 5.49ms
image

For details see #2952 (comment)

Continuation of #2948

https://particular.net/blog/10x-faster-execution-with-compiled-expression-trees

Expression compile causes too much GC call, application suspensions as a result. To see same picture on Visual Studio MyBenchmark.Main call FindValueOld in loop.

image

@gokhanabatay
Copy link
Contributor Author

gokhanabatay commented Dec 4, 2021

Benchmark results:
FindValue new implementation avoiding lambda copilation as much as possible.
FindValueOldis current FindValue compiles every time.

Expression

private static Expression GetExpression()
{
    (int, int) param = (1, 1);
    (DateTime, DateTime) date = (DateTime.Now, DateTime.Now);
    Expression<Func<string>> e = () => param.Item1.ToString() + Convert.ToInt32(date.Item2.ToString("yyyyMMdd"));
    return e.Body;
}

BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19042.1348 (20H2/October2020Update)
Intel Core i7-10510U CPU 1.80GHz, 1 CPU, 8 logical and 4 physical cores
.NET SDK=6.0.100
[Host] : .NET 6.0.0 (6.0.21.52210), X64 RyuJIT
DefaultJob : .NET 6.0.0 (6.0.21.52210), X64 RyuJIT

Method Mean Error StdDev Median Ratio
FindValueOld 549.610 us 7.4483 us 6.6027 us 548.786 us 1.000
FindValue 3.196 us 0.0632 us 0.1489 us 3.256 us 0.006

// * Hints *
Outliers
MyBenchmark.FindValueOld: Default -> 1 outlier was removed (569.50 us)

// * Legends *
Mean : Arithmetic mean of all measurements
Error : Half of 99.9% confidence interval
StdDev : Standard deviation of all measurements
Median : Value separating the higher half of all measurements (50th percentile)
Ratio : Mean of the ratio distribution ([Current]/[Baseline])
1 us : 1 Microsecond (0.000001 sec)

Source
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Linq.Expressions;
using System.Reflection;

namespace MyBenchmark
{

    public class MyBenchmark
    {
        public static object FindValueOld(Expression expression)
        {
            if (expression.NodeType == ExpressionType.Constant)
                return ((ConstantExpression)expression).Value;

            if (expression.NodeType == ExpressionType.MemberAccess)
            {
                var memberAccess = (MemberExpression)expression;
                if (memberAccess.Expression == null || memberAccess.Expression.NodeType == ExpressionType.Constant)
                {
                    var constantValue = ((ConstantExpression)memberAccess.Expression)?.Value;
                    var member = memberAccess.Member;
                    switch (member.MemberType)
                    {
                        case MemberTypes.Field:
                            return ((FieldInfo)member).GetValue(constantValue);
                        case MemberTypes.Property:
                            return ((PropertyInfo)member).GetValue(constantValue);
                    }
                }
            }

            var valueExpression = Expression.Lambda(expression).Compile();
            object value = valueExpression.DynamicInvoke();
            return value;
        }

        public static object FindValue(Expression expression)
        {
            object value;
            object[] args;
            Type type;
            switch (expression.NodeType)
            {
                case ExpressionType.Constant:
                    var constantExpression = (ConstantExpression)expression;
                    return constantExpression.Value;
                case ExpressionType.MemberAccess:
                    var memberExpression = (MemberExpression)expression;
                    value = memberExpression.Expression != null ? FindValue(memberExpression.Expression) : null;

                    switch (memberExpression.Member.MemberType)
                    {
                        case MemberTypes.Field:
                            return ((FieldInfo)memberExpression.Member).GetValue(value);
                        case MemberTypes.Property:
                            return ((PropertyInfo)memberExpression.Member).GetValue(value);
                    }
                    break;
                case ExpressionType.Convert:
                case ExpressionType.Not:
                case ExpressionType.Negate:
                case ExpressionType.UnaryPlus:
                case ExpressionType.Decrement:
                case ExpressionType.Increment:
                    var unaryExpression = (UnaryExpression)expression;
                    value = FindValue(unaryExpression.Operand);
                    switch (expression.NodeType)
                    {
                        case ExpressionType.Convert:
                            type = Nullable.GetUnderlyingType(unaryExpression.Type) ?? unaryExpression.Type;
                            if (type == typeof(object) || value == null)
                            {
                                return value;
                            }
                            else if (value is IConvertible || unaryExpression.Method != null)
                            {
                                if (type != unaryExpression.Operand.Type)
                                {
                                    value = unaryExpression.Method != null ? unaryExpression.Method.Invoke(null, new[] { value }) : Convert.ChangeType(value, type);
                                }

                                return value;
                            }
                            break;
                        case ExpressionType.Not:
                            if (value is bool)
                            {
                                return !(dynamic)value;
                            }
                            else
                            {
                                return ~(dynamic)value;
                            }
                        case ExpressionType.Negate:
                            return -(dynamic)value;
                        case ExpressionType.UnaryPlus:
                            return +(dynamic)value;
                        case ExpressionType.Decrement:
                            return (dynamic)value-1;
                        case ExpressionType.Increment:
                            return (dynamic)value+1;
                    }
                    break;
                case ExpressionType.Call:
                    var methodCallExpression = (MethodCallExpression)expression;
                    args = new object[methodCallExpression.Arguments.Count];
                    for (int i = 0; i < args.Length; i++)
                        args[i] = FindValue(methodCallExpression.Arguments[i]);

                    if (methodCallExpression.Object == null) //extension or static method
                    {
                        if (args.Length > 0 && args[0] != null)
                        {
                            return methodCallExpression.Method.Invoke(args[0].GetType(), args);
                        }
                        else
                        {
                            return methodCallExpression.Method.Invoke(methodCallExpression.Method.DeclaringType, args);
                        }
                    }
                    else
                    {
                        var callingObject = FindValue(methodCallExpression.Object);

                        return methodCallExpression.Method.Invoke(callingObject, args);
                    }
                case ExpressionType.Multiply:
                case ExpressionType.Divide:
                case ExpressionType.Modulo:
                case ExpressionType.Add:
                case ExpressionType.Subtract:
                case ExpressionType.Power:
                case ExpressionType.AndAlso:
                case ExpressionType.OrElse:
                case ExpressionType.And:
                case ExpressionType.Or:
                case ExpressionType.ExclusiveOr:
                case ExpressionType.Equal:
                case ExpressionType.NotEqual:
                case ExpressionType.LessThan:
                case ExpressionType.LessThanOrEqual:
                case ExpressionType.GreaterThan:
                case ExpressionType.GreaterThanOrEqual:
                case ExpressionType.ArrayIndex:
                case ExpressionType.Coalesce:
                case ExpressionType.LeftShift:
                case ExpressionType.RightShift:
                    var binaryExpression = (BinaryExpression)expression;
                    dynamic left = FindValue(binaryExpression.Left);
                    dynamic right = FindValue(binaryExpression.Right);
                    if (binaryExpression.Method != null)
                    {
                        return binaryExpression.Method.Invoke(null, new[] { left, right });
                    }
                    else
                    {
                        switch (expression.NodeType)
                        {
                            case ExpressionType.Multiply:
                                return left * right;
                            case ExpressionType.Divide:
                                return left / right;
                            case ExpressionType.Modulo:
                                return left % right;
                            case ExpressionType.Add:
                                return left + right;
                            case ExpressionType.Subtract:
                                return left - right;
                            case ExpressionType.Power:
                                return left ^ right;
                            case ExpressionType.AndAlso:
                                return left && right;
                            case ExpressionType.OrElse:
                                return left || right;
                            case ExpressionType.And:
                                return left & right;
                            case ExpressionType.Or:
                                return left | right;
                            case ExpressionType.ExclusiveOr:
                                return left ^ right;
                            case ExpressionType.Equal:
                                return left == right;
                            case ExpressionType.NotEqual:
                                return left != right;
                            case ExpressionType.LessThan:
                                return left < right;
                            case ExpressionType.LessThanOrEqual:
                                return left <= right;
                            case ExpressionType.GreaterThan:
                                return left > right;
                            case ExpressionType.GreaterThanOrEqual:
                                return left >= right;
                            case ExpressionType.ArrayIndex:
                                return left[right];
                            case ExpressionType.Coalesce:
                                return left ?? right;
                            case ExpressionType.LeftShift:
                                return left << right;
                            case ExpressionType.RightShift:
                                return left >> right;
                        }
                    }
                    break;
                case ExpressionType.Conditional:
                    var conditionalExpression = (ConditionalExpression)expression;
                    bool condition = (bool)FindValue(conditionalExpression.Test);
                    if (condition)
                    {
                        return FindValue(conditionalExpression.IfTrue);
                    }
                    else
                    {
                        return FindValue(conditionalExpression.IfFalse);
                    }
                case ExpressionType.NewArrayInit:
                    var newArrayExpression = (NewArrayExpression)expression;
                    type = newArrayExpression.Type.GetElementType();
                    if (type != null)
                    {
                        dynamic array = Array.CreateInstance(type, newArrayExpression.Expressions.Count);
                        for (int i = 0; i< newArrayExpression.Expressions.Count; i++)
                        {
                            array[i] = (dynamic)FindValue(newArrayExpression.Expressions[i]);
                        }
                        return array;
                    }
                    break;
                case ExpressionType.ListInit:
                    var listInitExpression = (ListInitExpression)expression;
                    var list = Activator.CreateInstance(listInitExpression.Type);
                    foreach (var item in listInitExpression.Initializers)
                    {
                        args = new object[item.Arguments.Count];
                        for (int i = 0; i < args.Length; i++)
                        {
                            args[i] = FindValue(item.Arguments[i]);
                        }

                        item.AddMethod.Invoke(list, args);
                    }
                    return list;
                case ExpressionType.New:
                    var newExpression = (NewExpression)expression;
                    args = new object[newExpression.Arguments.Count];
                    for (int i = 0; i < args.Length; i++)
                    {
                        args[i] = FindValue(newExpression.Arguments[i]);
                    }
                    return newExpression.Constructor.Invoke(args);
            }

            return Expression.Lambda(expression).Compile().DynamicInvoke();
        }

        private static Expression GetExpression()
        {
            (int, int) param = (1, 1);
            (DateTime, DateTime) date = (DateTime.Now, DateTime.Now);
            Expression<Func<object>> e = () => param.Item1.ToString() + Convert.ToInt32(date.Item2.ToString("yyyyMMdd"));
            return e.Body;
        }

        private static Expression GetExpression2()
        {
            Expression<Func<string>> e = () => new int[] { 1, 2 }.First().ToString();
            return e.Body;
        }

        private static Expression GetExpression3()
        {
            Expression<Func<List<int>>> e = () => new List<int>() { 1, 2 };
            return e.Body;
        }

        private static Expression GetExpression4()
        {
            int[] abc = new int[] { 1 };
            Expression<Func<int>> e = () => abc[0];
            return e.Body;
        }

        private static Expression GetExpression5()
        {
            Expression<Func<int>> e = () => new { Abcd = 5 }.Abcd;
            return e.Body;
        }

        private static Expression GetExpression6()
        {
            Random random = new();
            Expression<Func<bool>> e = () => !(random.Next(1, 3)==random.Next(1, 5));
            return e.Body;
        }

        private static Expression GetExpression7()
        {
            int a = 5;
            Expression<Func<int>> e = () => (-a);
            return e.Body;
        }

        private static Expression GetExpression8()
        {
            int a = 5;
            Expression<Func<int>> e = () => (a+1);
            return e.Body;
        }

        [Benchmark(Baseline = true)]
        public object FindValueOld()
        {
            return FindValueOld(GetExpression());
        }

        [Benchmark]
        public object FindValue()
        {
            return FindValue(GetExpression());
        }
    }

    public class Program
    {
        public static void Main(string[] args)
        {
            var summary = BenchmarkRunner.Run<MyBenchmark>();
        }
    }

    //public class Program
    //{
    //    public static void Main(string[] args)
    //    {
    //        MyBenchmark myBenchmark = new MyBenchmark();
    //        for (int i = 0; i < 1000000; i++)
    //        {
    //            myBenchmark.FindValue();
    //        }
    //        System.Console.WriteLine("The end");
    //        System.Console.ReadLine();
    //    }
    //}
}

@gokhanabatay

This comment has been minimized.

@bahusoid
Copy link
Member

bahusoid commented Dec 6, 2021

I'm not sure we should handle so many cases. If you really need perf - you should avoid calculations inside query expression (so you should move all calculations to variable outside of query)

For instance try your benchmark with the following modifications in FindValueOld:

Source
private static Expression GetExpressionCalcInParam()
{
    (int, int) param = (1, 1);
    (DateTime, DateTime) date = (DateTime.Now, DateTime.Now);
    var result = param.Item1.ToString() + Convert.ToInt32(date.Item2.ToString("yyyyMMdd"));
    Expression<Func<object>> e = () => result;
    return e.Body;
}

[Benchmark(Baseline = true)]
public object FindValueOld()
{
    return FindValueOld(GetExpressionCalcInParam());
}

[Benchmark]
public object FindValue()
{
    return FindValue(GetExpression());
}

I'm pretty sure it will be 2+ times faster than your version. So I would add support for Convert (as it's easy to miss implicit cast) and maybe Method call and that's it. But let's hear what other members think.

@gokhanabatay
Copy link
Contributor Author

gokhanabatay commented Dec 6, 2021

Hi @bahusoid,
We can remove some of the them, but as I mentioned above, the current implemtation causes GC(suspencions) problems at high tps and serious performance problems occurs in applications.

We should avoid lambda compilation as much as possible which is the current production performance issue for us.
And I'm sure most NHibernate users are unaware of this kind of performance issue.
We can write queries with calculations, linq allows us to do that and we can deal with this implementation.

Queries do not include calculations or other controls does not affected this code.
We must handle:
-Nested Property expression Where(x=>x.JobStatus == jobStatusRequest.Job.Status) (to do that FindValue needs to be recursive)
-Method Call expression Where(x=>x.RequestDate == jobStatusRequest.RequestDate.ToString("yyyyMMdd"))
-Conversion expression
-Conditional expression Where(x=>x.IsInProgress == (jobStatusRequest.Job.Status == "Active" ? 1 : 0))
-Common binary expression Where(x=>x.JobStatus == (jobStatusRequest.Job.Status ?? "Active"))
-Array or list init expressions for queries Where(x=> new string[]{"Active","Waiting"}.Contains(x.JobStatus))

GC(suspencions) is a big issue, if you check below usages of ExpressionProcessor.FindValue you can see it could affect other usages like QueryOver<>
image

We can handle all of above so why not, we can support as you wish, please consider it.

Finally you are wright, If we move them from expression it will be faster but if linq lets us to write this and if its causes GC(suspensions) we need to fix it as much as possible.

@gokhanabatay
Copy link
Contributor Author

@razzmatazz could you test with this version, you have reported same issues recently

@gokhanabatay

This comment has been minimized.

@bahusoid

This comment has been minimized.

@gokhanabatay

This comment has been minimized.

@bahusoid

This comment has been minimized.

@gokhanabatay
Copy link
Contributor Author

Hi @bahusoid,

Compile(true) preferInterpretation true to indicate that the expression should be compiled to an interpreted form, if it is available; false otherwise. When I set true GC call decreased, so we may remove some of blocks.

FindValueV2, FindValueV3 What dou you think? if its ok I can change and push the code?

Tested with

private static Expression GetExpression()
{
    Expression<Func<int>> e = () => Convert.ToInt32(DateTime.Now.ToString("yyyyMMdd"));
    return e.Body;
}
Source
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Linq.Expressions;
using System.Reflection;

namespace MyBenchmark
{

    public class MyBenchmark
    {
        public static object FindValueOld(Expression expression)
        {
            if (expression.NodeType == ExpressionType.Constant)
                return ((ConstantExpression)expression).Value;

            if (expression.NodeType == ExpressionType.MemberAccess)
            {
                var memberAccess = (MemberExpression)expression;
                if (memberAccess.Expression == null || memberAccess.Expression.NodeType == ExpressionType.Constant)
                {
                    var constantValue = ((ConstantExpression)memberAccess.Expression)?.Value;
                    var member = memberAccess.Member;
                    switch (member.MemberType)
                    {
                        case MemberTypes.Field:
                            return ((FieldInfo)member).GetValue(constantValue);
                        case MemberTypes.Property:
                            return ((PropertyInfo)member).GetValue(constantValue);
                    }
                }
            }

            return Expression.Lambda(expression).Compile().DynamicInvoke();
        }

        public static object FindValue(Expression expression)
        {
            object value;
            object[] args;
            Type type;
            switch (expression.NodeType)
            {
                case ExpressionType.Constant:
                    var constantExpression = (ConstantExpression)expression;
                    return constantExpression.Value;
                case ExpressionType.MemberAccess:
                    var memberExpression = (MemberExpression)expression;
                    value = memberExpression.Expression != null ? FindValue(memberExpression.Expression) : null;

                    switch (memberExpression.Member.MemberType)
                    {
                        case MemberTypes.Field:
                            return ((FieldInfo)memberExpression.Member).GetValue(value);
                        case MemberTypes.Property:
                            return ((PropertyInfo)memberExpression.Member).GetValue(value);
                    }
                    break;
                case ExpressionType.Convert:
                case ExpressionType.Not:
                case ExpressionType.Negate:
                case ExpressionType.UnaryPlus:
                case ExpressionType.Decrement:
                case ExpressionType.Increment:
                    var unaryExpression = (UnaryExpression)expression;
                    value = FindValue(unaryExpression.Operand);
                    switch (expression.NodeType)
                    {
                        case ExpressionType.Convert:
                            type = Nullable.GetUnderlyingType(unaryExpression.Type) ?? unaryExpression.Type;
                            if (type == typeof(object) || value == null)
                            {
                                return value;
                            }
                            else if (value is IConvertible || unaryExpression.Method != null)
                            {
                                if (type != unaryExpression.Operand.Type)
                                {
                                    value = unaryExpression.Method != null ? unaryExpression.Method.Invoke(null, new[] { value }) : Convert.ChangeType(value, type);
                                }

                                return value;
                            }
                            break;
                        case ExpressionType.Not:
                            if (value is bool)
                            {
                                return !(dynamic)value;
                            }
                            else
                            {
                                return ~(dynamic)value;
                            }
                        case ExpressionType.Negate:
                            return -(dynamic)value;
                        case ExpressionType.UnaryPlus:
                            return +(dynamic)value;
                        case ExpressionType.Decrement:
                            return (dynamic)value-1;
                        case ExpressionType.Increment:
                            return (dynamic)value+1;
                    }
                    break;
                case ExpressionType.Call:
                    var methodCallExpression = (MethodCallExpression)expression;
                    args = new object[methodCallExpression.Arguments.Count];
                    for (int i = 0; i < args.Length; i++)
                        args[i] = FindValue(methodCallExpression.Arguments[i]);

                    if (methodCallExpression.Object == null) //extension or static method
                    {
                        if (args.Length > 0 && args[0] != null)
                        {
                            return methodCallExpression.Method.Invoke(args[0].GetType(), args);
                        }
                        else
                        {
                            return methodCallExpression.Method.Invoke(methodCallExpression.Method.DeclaringType, args);
                        }
                    }
                    else
                    {
                        var callingObject = FindValue(methodCallExpression.Object);

                        return methodCallExpression.Method.Invoke(callingObject, args);
                    }
                case ExpressionType.Multiply:
                case ExpressionType.Divide:
                case ExpressionType.Modulo:
                case ExpressionType.Add:
                case ExpressionType.Subtract:
                case ExpressionType.Power:
                case ExpressionType.AndAlso:
                case ExpressionType.OrElse:
                case ExpressionType.And:
                case ExpressionType.Or:
                case ExpressionType.ExclusiveOr:
                case ExpressionType.Equal:
                case ExpressionType.NotEqual:
                case ExpressionType.LessThan:
                case ExpressionType.LessThanOrEqual:
                case ExpressionType.GreaterThan:
                case ExpressionType.GreaterThanOrEqual:
                case ExpressionType.ArrayIndex:
                case ExpressionType.Coalesce:
                case ExpressionType.LeftShift:
                case ExpressionType.RightShift:
                    var binaryExpression = (BinaryExpression)expression;
                    dynamic left = FindValue(binaryExpression.Left);
                    dynamic right = FindValue(binaryExpression.Right);
                    if (binaryExpression.Method != null)
                    {
                        return binaryExpression.Method.Invoke(null, new[] { left, right });
                    }
                    else
                    {
                        switch (expression.NodeType)
                        {
                            case ExpressionType.Multiply:
                                return left * right;
                            case ExpressionType.Divide:
                                return left / right;
                            case ExpressionType.Modulo:
                                return left % right;
                            case ExpressionType.Add:
                                return left + right;
                            case ExpressionType.Subtract:
                                return left - right;
                            case ExpressionType.Power:
                                return left ^ right;
                            case ExpressionType.AndAlso:
                                return left && right;
                            case ExpressionType.OrElse:
                                return left || right;
                            case ExpressionType.And:
                                return left & right;
                            case ExpressionType.Or:
                                return left | right;
                            case ExpressionType.ExclusiveOr:
                                return left ^ right;
                            case ExpressionType.Equal:
                                return left == right;
                            case ExpressionType.NotEqual:
                                return left != right;
                            case ExpressionType.LessThan:
                                return left < right;
                            case ExpressionType.LessThanOrEqual:
                                return left <= right;
                            case ExpressionType.GreaterThan:
                                return left > right;
                            case ExpressionType.GreaterThanOrEqual:
                                return left >= right;
                            case ExpressionType.ArrayIndex:
                                return left[right];
                            case ExpressionType.Coalesce:
                                return left ?? right;
                            case ExpressionType.LeftShift:
                                return left << right;
                            case ExpressionType.RightShift:
                                return left >> right;
                        }
                    }
                    break;
                case ExpressionType.Conditional:
                    var conditionalExpression = (ConditionalExpression)expression;
                    bool condition = (bool)FindValue(conditionalExpression.Test);
                    if (condition)
                    {
                        return FindValue(conditionalExpression.IfTrue);
                    }
                    else
                    {
                        return FindValue(conditionalExpression.IfFalse);
                    }
                case ExpressionType.NewArrayInit:
                    var newArrayExpression = (NewArrayExpression)expression;
                    type = newArrayExpression.Type.GetElementType();
                    if (type != null)
                    {
                        dynamic array = Array.CreateInstance(type, newArrayExpression.Expressions.Count);
                        for (int i = 0; i< newArrayExpression.Expressions.Count; i++)
                        {
                            array[i] = (dynamic)FindValue(newArrayExpression.Expressions[i]);
                        }
                        return array;
                    }
                    break;
                case ExpressionType.ListInit:
                    var listInitExpression = (ListInitExpression)expression;
                    var list = Activator.CreateInstance(listInitExpression.Type);
                    foreach (var item in listInitExpression.Initializers)
                    {
                        args = new object[item.Arguments.Count];
                        for (int i = 0; i < args.Length; i++)
                        {
                            args[i] = FindValue(item.Arguments[i]);
                        }

                        item.AddMethod.Invoke(list, args);
                    }
                    return list;
                case ExpressionType.New:
                    var newExpression = (NewExpression)expression;
                    args = new object[newExpression.Arguments.Count];
                    for (int i = 0; i < args.Length; i++)
                    {
                        args[i] = FindValue(newExpression.Arguments[i]);
                    }
                    return newExpression.Constructor.Invoke(args);
            }

            return Expression.Lambda(expression).Compile().DynamicInvoke();
        }

        public static object FindValueV2(Expression expression)
        {
            object value;
            switch (expression.NodeType)
            {
                case ExpressionType.Constant:
                    var constantExpression = (ConstantExpression)expression;
                    return constantExpression.Value;
                case ExpressionType.MemberAccess:
                    var memberExpression = (MemberExpression)expression;
                    value = memberExpression.Expression != null ? FindValueV2(memberExpression.Expression) : null;

                    switch (memberExpression.Member.MemberType)
                    {
                        case MemberTypes.Field:
                            return ((FieldInfo)memberExpression.Member).GetValue(value);
                        case MemberTypes.Property:
                            return ((PropertyInfo)memberExpression.Member).GetValue(value);
                    }
                    break;
                case ExpressionType.Call:
                    var methodCallExpression = (MethodCallExpression)expression;
                    var args = new object[methodCallExpression.Arguments.Count];
                    for (int i = 0; i < args.Length; i++)
                        args[i] = FindValueV2(methodCallExpression.Arguments[i]);

                    if (methodCallExpression.Object == null) //extension or static method
                    {
                        if (args.Length > 0 && args[0] != null)
                        {
                            return methodCallExpression.Method.Invoke(args[0].GetType(), args);
                        }
                        else
                        {
                            return methodCallExpression.Method.Invoke(methodCallExpression.Method.DeclaringType, args);
                        }
                    }
                    else
                    {
                        var callingObject = FindValueV2(methodCallExpression.Object);

                        return methodCallExpression.Method.Invoke(callingObject, args);
                    }
                case ExpressionType.Convert:
                    var unaryExpression = (UnaryExpression)expression;
                    value = FindValueV2(unaryExpression.Operand);
                    var type = Nullable.GetUnderlyingType(unaryExpression.Type) ?? unaryExpression.Type;
                    if (type == typeof(object) || value == null)
                    {
                        return value;
                    }
                    else if (value is IConvertible || unaryExpression.Method != null)
                    {
                        if (type != unaryExpression.Operand.Type)
                        {
                            value = unaryExpression.Method != null ? unaryExpression.Method.Invoke(null, new[] { value }) : Convert.ChangeType(value, type);
                        }

                        return value;
                    }
                    break;
                case ExpressionType.Multiply:
                case ExpressionType.Divide:
                case ExpressionType.Modulo:
                case ExpressionType.Add:
                case ExpressionType.Subtract:
                case ExpressionType.Power:
                case ExpressionType.AndAlso:
                case ExpressionType.OrElse:
                case ExpressionType.And:
                case ExpressionType.Or:
                case ExpressionType.Equal:
                case ExpressionType.NotEqual:
                case ExpressionType.LessThan:
                case ExpressionType.LessThanOrEqual:
                case ExpressionType.GreaterThan:
                case ExpressionType.GreaterThanOrEqual:
                case ExpressionType.ArrayIndex:
                case ExpressionType.Coalesce:
                    var binaryExpression = (BinaryExpression)expression;
                    dynamic left = FindValueV2(binaryExpression.Left);
                    dynamic right = FindValueV2(binaryExpression.Right);
                    if (binaryExpression.Method != null)
                    {
                        return binaryExpression.Method.Invoke(null, new[] { left, right });
                    }
                    else
                    {
                        switch (expression.NodeType)
                        {
                            case ExpressionType.Multiply:
                                return left * right;
                            case ExpressionType.Divide:
                                return left / right;
                            case ExpressionType.Modulo:
                                return left % right;
                            case ExpressionType.Add:
                                return left + right;
                            case ExpressionType.Subtract:
                                return left - right;
                            case ExpressionType.Power:
                                return left ^ right;
                            case ExpressionType.AndAlso:
                                return left && right;
                            case ExpressionType.OrElse:
                                return left || right;
                            case ExpressionType.And:
                                return left & right;
                            case ExpressionType.Or:
                                return left | right;
                            case ExpressionType.Equal:
                                return left == right;
                            case ExpressionType.NotEqual:
                                return left != right;
                            case ExpressionType.LessThan:
                                return left < right;
                            case ExpressionType.LessThanOrEqual:
                                return left <= right;
                            case ExpressionType.GreaterThan:
                                return left > right;
                            case ExpressionType.GreaterThanOrEqual:
                                return left >= right;
                            case ExpressionType.ArrayIndex:
                                return left[right];
                            case ExpressionType.Coalesce:
                                return left ?? right;
                        }
                    }
                    break;
            }

            return Expression.Lambda(expression).Compile(true).DynamicInvoke();
        }

        public static object FindValueV3(Expression expression)
        {
            object value;
            switch (expression.NodeType)
            {
                case ExpressionType.Constant:
                    var constantExpression = (ConstantExpression)expression;
                    return constantExpression.Value;
                case ExpressionType.MemberAccess:
                    var memberExpression = (MemberExpression)expression;
                    value = memberExpression.Expression != null ? FindValueV3(memberExpression.Expression) : null;

                    switch (memberExpression.Member.MemberType)
                    {
                        case MemberTypes.Field:
                            return ((FieldInfo)memberExpression.Member).GetValue(value);
                        case MemberTypes.Property:
                            return ((PropertyInfo)memberExpression.Member).GetValue(value);
                    }
                    break;
                case ExpressionType.Call:
                    var methodCallExpression = (MethodCallExpression)expression;
                    var args = new object[methodCallExpression.Arguments.Count];
                    for (int i = 0; i < args.Length; i++)
                        args[i] = FindValueV3(methodCallExpression.Arguments[i]);

                    if (methodCallExpression.Object == null) //extension or static method
                    {
                        if (args.Length > 0 && args[0] != null)
                        {
                            return methodCallExpression.Method.Invoke(args[0].GetType(), args);
                        }
                        else
                        {
                            return methodCallExpression.Method.Invoke(methodCallExpression.Method.DeclaringType, args);
                        }
                    }
                    else
                    {
                        var callingObject = FindValueV3(methodCallExpression.Object);

                        return methodCallExpression.Method.Invoke(callingObject, args);
                    }
                case ExpressionType.Convert:
                    var unaryExpression = (UnaryExpression)expression;
                    value = FindValueV3(unaryExpression.Operand);
                    var type = Nullable.GetUnderlyingType(unaryExpression.Type) ?? unaryExpression.Type;
                    if (type == typeof(object) || value == null)
                    {
                        return value;
                    }
                    else if (value is IConvertible || unaryExpression.Method != null)
                    {
                        if (type != unaryExpression.Operand.Type)
                        {
                            value = unaryExpression.Method != null ? unaryExpression.Method.Invoke(null, new[] { value }) : Convert.ChangeType(value, type);
                        }

                        return value;
                    }
                    break;
            }

            return Expression.Lambda(expression).Compile(true).DynamicInvoke();
        }

        private static Expression GetExpression()
        {
            Expression<Func<int>> e = () => Convert.ToInt32(DateTime.Now.ToString("yyyyMMdd"));
            return e.Body;
        }

        private static Expression GetExpressionCalcInParam()
        {
            (int, int) param = (1, 1);
            (DateTime, DateTime) date = (DateTime.Now, DateTime.Now);
            var result = param.Item1.ToString() + Convert.ToInt32(date.Item2.ToString("yyyyMMdd"));
            Expression<Func<object>> e = () => result;
            return e.Body;
        }

        [Benchmark(Baseline = true)]
        public object FindValueOld()
        {
            return FindValueOld(GetExpression());
        }

        [Benchmark]
        public object FindValue()
        {
            return FindValue(GetExpression());
        }

        [Benchmark]
        public object FindValueV2()
        {
            return FindValueV2(GetExpression());
        }

        [Benchmark]
        public object FindValueV3()
        {
            return FindValueV3(GetExpression());
        }
    }

    public class Program
    {
        public static void Main(string[] args)
        {
            var summary = BenchmarkRunner.Run<MyBenchmark>();
        }
    }

    //public class Program
    //{
    //    public static void Main(string[] args)
    //    {
    //        MyBenchmark myBenchmark = new MyBenchmark();
    //        for (int i = 0; i < 5000000; i++)
    //        {
    //            myBenchmark.FindValueV3();
    //        }
    //        System.Console.WriteLine("The end");
    //        System.Console.ReadLine();
    //    }
    //}
}

image

@bahusoid
Copy link
Member

bahusoid commented Dec 7, 2021

Compile(true)

Good finding! Yeah sure we should use it. But does it make any sense to add anything at all to existing code except Compile(true) call?
Unfortunately it's not available under .net461 so you need to use something like this code:

            var valueExpression = Expression.Lambda(expression)
#if NETCOREAPP2_0_OR_GREATER || NET47_OR_GREATER || NETSTANDARD2_0_OR_GREATER
                .Compile(true);
#else
                .Compile();
#endif
            object value = valueExpression.DynamicInvoke();
            return value;

@gokhanabatay
Copy link
Contributor Author

gokhanabatay commented Dec 7, 2021

Compile(true)

Good finding! Yeah sure we should use it. But does it make any sense to add anything at all to existing code except Compile(true) call?

Thank you, existing code is too limited, if you ok with that I would like to use FindValue3

FindValu3 has capable wihout compilation with below usages:
-Nested Property/Field expression Where(x=>x.JobStatus == jobStatusRequest.Job.Status) (to do that FindValue needs to be recursive)
-Method Call expression Where(x=>x.RequestDate == jobStatusRequest.RequestDate.ToString("yyyyMMdd"))
-Conversion expression, did you see any issue with this method?

If we change FindValueOld with your suggestion
image

@bahusoid
Copy link
Member

bahusoid commented Dec 7, 2021

if you ok with that I would like to use FindValue3

OK

@bahusoid
Copy link
Member

bahusoid commented Dec 7, 2021

-Conversion expression, did you see any issue with this method?

I will look at it later. But you would need to add tests for it (implicit conversion int -> long, valid up convert - Entity -> SubEntity, invalid convert like ((string)(object)(int)1)), At a quick glance: I don't like IConvertible stuff at all - most likely we don't need it.

@gokhanabatay
Copy link
Contributor Author

I will look at it later. But you would need to add tests for it (implicit conversion int -> long, valid up convert - Entity -> SubEntity, invalid convert like ((string)(object)(int)1)), At a quick glance: I don't like IConvertible stuff at all - most likely we don't need it.

Me too but these test has been failed without IConvertible, dou you have other idea? https://teamcity.jetbrains.com/viewLog.html?buildId=3720513&buildTypeId=bt7

If you help me a little bit(such as simple test) I will add additinoal tests in it.

var valueExpression = Expression.Lambda(expression).Compile();
object value = valueExpression.DynamicInvoke();
var valueExpression = Expression.Lambda(expression)
#if NETCOREAPP2_0_OR_GREATER || NET47_OR_GREATER || NETSTANDARD2_0_OR_GREATER
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please create an extension method instead:

#if NET461
internal static class LambdaExpressionExtensions 
{
  public Delegate Compile (this LambdaExpression expression, bool preferInterpretation) =>
    expression.Compile();    
}
#endif

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means a net47-48 application cannot benefit from this optimization until we upgrade the NHibernate target to net47, right? (Or add a target for net48 by example.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think community needs to discuss, to apply this fix to older versions of nhibernate for fixing GC issue.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fredericDelaporte it is essentially the same code just shifted to an extension method to reduce noise. I did not do any thinking beyond that. We probably can build a runtime shim to discover the underlying method (as we do for other stuff).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are going to add net60 target to 5.4 than why not also add net47 (or net48 but seems net48 doesn't add anything useful for us). I would prefer to add target rather than adding such shims.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MS recommends adding 4.8

Copy link
Member

@bahusoid bahusoid Dec 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

…ll causes "Garbage Collector" suspend all threads too often
@gokhanabatay gokhanabatay force-pushed the AvoidLambdaCompilation branch from fa355ff to f1586a9 Compare December 7, 2021 10:16
Copy link
Member

@bahusoid bahusoid left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This Convert is a tricky one - we definitely need tests. Added some comments below but think yourself for other cases and write test cases...

You can add separate test fixture like ExpressionProcessorFindValueFixture put it near ExpressionProcessorFixture. And tests could look something like:

        private static object GetValue<T>(Expression<Func<T>> expression)
        {
            try
            {
                return ExpressionProcessor.FindValue(expression.Body);
            }
            catch (TargetInvocationException e)
            {
                throw e.InnerException;
            }
        }
        
        [Test]
        public void SomeInvalidConversions()
        {
            object val = 1;

            Assert.Throws<InvalidCastException>(() =>
                GetValue(() => (string)val));
        }

@gokhanabatay
Copy link
Contributor Author

@bahusoid first of all thank you for your guidence, feels nice to contribute.
Last question its production issue for us, when you merge into master we can use with dev builds.

Shall we expect a merge to master this week?

bahusoid
bahusoid previously approved these changes Dec 8, 2021
bahusoid
bahusoid previously approved these changes Dec 9, 2021
}

[Test]
public void Int16ToIntegerImplicitCast()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would fall into 'lambda compile' path. Do we want to fix it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have tried to fix it bu convert is to much tricky dou you have any idea to fix it ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I said in #2957 (comment)

Let's start from something simple but safe and make it more complex and sophisticated in other PRs. Unless you already have in mind some simple fix that you want to stick in this PR.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless you already have in mind some simple fix

It is a variation of ConvertType in the expression processor. Convert for primitive types (IsPrimitive):

The primitive types are Boolean, Byte, SByte, Int16, UInt16, Int32, UInt32, Int64, UInt64, IntPtr, UIntPtr, Char, Double, and Single.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a variation of ConvertType

I would do it in another PR. It doesn't look as straightforward fix to me. For instance Boolean type and value overflows might behave differently in runtime vs ChangeType.

Copy link
Contributor Author

@gokhanabatay gokhanabatay Dec 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @bahusoid, in my test I was stuck with NestedPropertyToIntegerCast and StringToIntegerCastFails failed with Convert.ChangeType
Such as
Convert.ChangeType(9.89m, typeof(int)) = 10, (int)9.89m = 9
Convert.ChangeType(true, typeof(int)) = 1, (int)true = fails

I also tried with T Cast<T>(object value) => return (T)value; store static instance of CastMethodInfo and make generic method before invoke but this was also failed. Which may run for primitive cases, for performance i am not sure of it? @hazzik

@gokhanabatay
Copy link
Contributor Author

@bahusoid @hazzik @fredericDelaporte what dou you think about merging this pull request to master, if you need any other test or improvment I can do it.

@bahusoid
Copy link
Member

I think it's good to go.. @hazzik Any objections?

@hazzik
Copy link
Member

hazzik commented Dec 13, 2021

I'm not really happy with the direction where this PR is going. But I dont have any objections against merging the PR.

@gokhanabatay
Copy link
Contributor Author

gokhanabatay commented Dec 13, 2021

I'm not really happy with the direction where this PR is going. But I dont have any objections against merging the PR.

Why are u not really happy @hazzik?
If u want additional changes we can work with another pr, now we are really suffering suspensions.

It has been a very enjoyable work for me and I am very happy to work with you. Thank you.

@hazzik
Copy link
Member

hazzik commented Dec 14, 2021

Why are u not really happy @hazzik?

Something feels wrong about the approach here, however I could not properly articulate what is wrong and how to make it right.

I think this probably should to be done somewhere else. Perhaps, together at the stage where the independent sub-trees are identified. Also, this implementation, in some circumstances could lead to additional compilations of lambdas instead of suppressing them.

Anyway, just for clarification: I don't have any objections against merging this PR as is. And there is certainly nothing personal.

@gokhanabatay
Copy link
Contributor Author

Thanks @hazzik, this seems to be the only way to prevent lambda compilations that I can find for now.

We look forward to merge of this pull request :)

@bahusoid bahusoid merged commit e0384a3 into nhibernate:master Dec 14, 2021
@gliljas
Copy link
Member

gliljas commented Dec 14, 2021

Just for good measure, I tested two other approaches.

  1. Use FastExpressionCompiler
  2. Use the absolutely ancient CachedExpressionCompiler

#1 rendered a 30% improvement over FindValueOld
#2 was almost as fast as the new FindValue

I believe the best solution is a combination of approaches, but for now the reflection approach is, somewhat surprisingly, the fastest.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants