dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
https://docs.microsoft.com/ef/
MIT License
13.81k stars 3.2k forks source link

Query/Perf: don't compile liftable constant resolvers in interpretation mode when the resolver itself contains a lambda #35208

Open maumar opened 3 days ago

maumar commented 3 days ago

This is part of a bigger perf regression between EF8 and EF9: https://github.com/dotnet/efcore/issues/35053

When processing LiftableConstantExpressions in the regular (non-AOT) mode we compile the resolver lambda and then evaluate it to get back the actual constant we plan to use. We do the compilation in the interpretation mode and that causes problems if the resulting object is itself (or contains) a delegate. Compiling into a delegate using interpreted mode, has impact on allocations as well as execution speed.

before the fix (this already includes the invoke fix https://github.com/dotnet/efcore/issues/35206)

Method Async Mean Error StdDev Op/s Gen0 Gen1 Allocated
MultiInclue False 487.1 ms 0.99 ms 0.88 ms 2.053 17000.0000 11000.0000 103.29 MB
MultiInclue True 487.4 ms 3.63 ms 3.22 ms 2.052 17000.0000 11000.0000 103.29 MB

after the fix

Method Async Mean Error StdDev Op/s Gen0 Gen1 Allocated
MultiInclue False 455.1 ms 8.94 ms 10.29 ms 2.197 11000.0000 6000.0000 67.92 MB
MultiInclue True 435.4 ms 1.77 ms 1.66 ms 2.297 11000.0000 6000.0000 67.92 MB
maumar commented 3 days ago

smoking gun benchmark:

    internal class Program
    {
        static void Main(string[] args)
        {
            BenchmarkRunner.Run(args, typeof(Program).Assembly);
        }
    }

    public static class BenchmarkRunner
    {
        public static void Run(string[] args, Assembly assembly, IConfig config = null)
        {
            config ??= DefaultConfig.Instance;

            config = config
                .AddDiagnoser(MemoryDiagnoser.Default)
                .AddColumn(StatisticColumn.OperationsPerSecond);

            BenchmarkSwitcher.FromAssembly(assembly).Run(args, config);
        }
    }

    public class InterpretationBenchmark
    {
        private Expression<Func<object, Func<object?, object?, bool>[]>> _expression;
        private Func<object, object, bool>[] _compiled;
        private Func<object, object, bool>[] _compiledInterpreted;

        [GlobalSetup]
        public virtual void Initialize()
        {
            var left = Expression.Parameter(typeof(object), "left");
            var right = Expression.Parameter(typeof(object), "right");

            var prm = Expression.Parameter(typeof(object));
            _expression = Expression.Lambda<Func<object, Func<object?, object?, bool>[]>>(
                Expression.NewArrayInit(
                    typeof(Func<object, object, bool>),
                    Expression.Lambda<Func<object?, object?, bool>>(
                        Expression.Condition(
                            Expression.Equal(left, Expression.Constant(null)),
                            Expression.Equal(right, Expression.Constant(null)),
                            Expression.AndAlso(
                                Expression.NotEqual(right, Expression.Constant(null)),
                                Expression.Equal(
                                    Expression.Convert(left, typeof(int)),
                                    Expression.Convert(left, typeof(int))))),
                      left,
                      right)), prm);

            _compiled = _expression.Compile()("_");
            _compiledInterpreted = _expression.Compile(preferInterpretation: true)("_");
        }

        [Benchmark]
        public virtual void Compiled()
        {
            var equal = 0;
            for (var i = 0; i < 100; i++)
            {
                for (var j = 0; j < 100; j++)
                {
                    if (_compiled[0](i, j))
                    {
                        equal++;
                    }
                }
            }
        }

        [Benchmark]
        public virtual void CompiledInterpolated()
        {
            var equal = 0;
            for (var i = 0; i < 100; i++)
            {
                for (var j = 0; j < 100; j++)
                {
                    if (_compiledInterpreted[0](i, j))
                    {
                        equal++;
                    }
                }
            }
        }
    }

results:

Method Mean Error StdDev Op/s Gen0 Allocated
Compiled 65.69 us 0.706 us 0.660 us 15,223.4 76.4160 468.75 KB
CompiledInterpolated 930.20 us 7.046 us 6.591 us 1,075.0 433.5938 2656.25 KB
maumar commented 3 days ago

reopening for potential patch