简体   繁体   English

使用反射构建 EF Core 查询比使用反射更快

[英]Using Reflection to build EF Core query is faster than to using reflection

I've got an IQueryable Extension method that is being used to reduce the amount of boiler plate code needed to search a number of fields in an EF Core DbContext model:我有一个 IQueryable 扩展方法,用于减少在 EF Core DbContext 模型中搜索多个字段所需的样板代码量:

public static IQueryable<TEntity> WherePropertyIsLikeIfStringIsNotEmpty<TEntity>(this IQueryable<TEntity> query,
    string searchValue, Expression<Func<TEntity, string>> propertySelectorExpression)
{
    if (string.IsNullOrEmpty(searchValue) || !(propertySelectorExpression.Body is MemberExpression memberExpression))
    {
        return query;
    }
    
    // get method info for EF.Functions.Like
    var likeMethod = typeof(DbFunctionsExtensions).GetMethod(nameof(DbFunctionsExtensions.Like), new []
    {
        typeof(DbFunctions),
        typeof(string),
        typeof(string)
    });
    var searchValueConstant = Expression.Constant($"%{searchValue}%");
    var dbFunctionsConstant = Expression.Constant(EF.Functions);
    var propertyInfo = typeof(TEntity).GetProperty(memberExpression.Member.Name);
    var parameterExpression = Expression.Parameter(typeof(TEntity));
    var propertyExpression = Expression.Property(parameterExpression, propertyInfo);
    
    
    var callLikeExpression = Expression.Call(likeMethod, dbFunctionsConstant, propertyExpression, searchValueConstant);
    var lambda = Expression.Lambda<Func<TEntity, bool>>(callLikeExpression, parameterExpression);
    return query.Where(lambda);
}

The code is working and producing expected results, however I was worried that I would get a performance hit for using Expressions and a bit of reflection.代码正在运行并产生预期的结果,但是我担心使用表达式和一些反射会影响性能。 So I set up a benchmark using an in memory database and the BenchmarkDotNet nuget package.所以我使用内存数据库和 BenchmarkDotNet nuget 包设置了一个基准测试。 Here is the benchmark:这是基准:

using System;
using System.Collections.Generic;
using System.Linq;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using Microsoft.EntityFrameworkCore;

class Program
    {
        static void Main(string[] args)
        {
            BenchmarkRunner.Run<Benchmark>();
        }
    }

    public class Benchmark
    {
        private Context _context;
        private string SearchValue1 = "BCD";
        private string SearchValue2 = "FG";
        private string SearchValue3 = "IJ";
        
        [GlobalSetup]
        public void Setup()
        {
            _context = new Context(new DbContextOptionsBuilder<Context>().UseInMemoryDatabase(Guid.NewGuid().ToString())
                .Options);

            _context.TestModels.Add(new TestModel(1, "ABCD", "EFGH", "HIJK"));
            _context.SaveChanges();
        }

        [GlobalCleanup]
        public void Cleanup()
        {
            _context.Dispose();
        }
        
        [Benchmark]
        public void FilterUsingExtension()
        {
            var _ = _context.TestModels
                .WherePropertyIsLikeIfStringIsNotEmpty(SearchValue1, testModel => testModel.Value)
                .WherePropertyIsLikeIfStringIsNotEmpty(SearchValue2, testModel => testModel.OtherValue)
                .WherePropertyIsLikeIfStringIsNotEmpty(SearchValue3, testModel => testModel.ThirdValue)
                .ToList();
        }

        [Benchmark]
        public void FilterTraditionally()
        {
            var query = _context.TestModels.AsQueryable();
            if (!string.IsNullOrEmpty(SearchValue1))
            {
                query = query.Where(x => EF.Functions.Like(x.Value, $"%{SearchValue1}%"));
            }
            if (!string.IsNullOrEmpty(SearchValue2))
            {
                query = query.Where(x => EF.Functions.Like(x.OtherValue, $"%{SearchValue2}%"));
            }
            if (!string.IsNullOrEmpty(SearchValue3))
            {
                query = query.Where(x => EF.Functions.Like(x.ThirdValue, $"%{SearchValue3}%"));
            }
        
            var _ = query.ToList();
        }
    }

    public class TestModel
    {
        public int Id { get; }
        public string Value { get; }
        public string OtherValue { get; }
        public string ThirdValue { get; }

        public TestModel(int id, string value, string otherValue, string thirdValue)
        {
            Id = id;
            Value = value;
            OtherValue = otherValue;
            ThirdValue = thirdValue;
        }
    }
    
    public class Context : DbContext
    {

        public Context(DbContextOptions<Context> options)
            : base(options)
        {
            
        }
        
        // ReSharper disable once UnusedAutoPropertyAccessor.Global
        public DbSet<TestModel> TestModels { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            modelBuilder.Entity<TestModel>().ToTable("test_class", "test");
            modelBuilder.Entity<TestModel>().Property(x => x.Id).HasColumnName("id").HasColumnType("int");
            modelBuilder.Entity<TestModel>().Property(x => x.Value).HasColumnName("value").HasColumnType("varchar")
                .ValueGeneratedNever();
            modelBuilder.Entity<TestModel>().Property(x => x.OtherValue).HasColumnName("other_value").HasColumnType("varchar")
                .ValueGeneratedNever();
            modelBuilder.Entity<TestModel>().Property(x => x.ThirdValue).HasColumnName("third_value").HasColumnType("varchar")
                .ValueGeneratedNever();
            modelBuilder.Entity<TestModel>().HasKey(x => x.Id);
        }
    }

Like I said, I was expecting performance penalties for using reflection.就像我说的,我期待使用反射的性能损失。 but the benchmark shows that the query being built by my extension method is more than 10 times faster than just writing the expression directly in the Where method:但基准测试表明,由我的扩展方法构建的查询比直接在 Where 方法中编写表达式快 10 倍以上:

|               Method |        Mean |     Error |    StdDev |      Median |
|--------------------- |------------:|----------:|----------:|------------:|
| FilterUsingExtension |    73.73 us |  1.381 us |  3.310 us |    72.36 us |
|  FilterTraditionally | 1,036.60 us | 20.494 us | 22.779 us | 1,032.69 us |

Can anyone give an explanation for this?任何人都可以对此做出解释吗?

Shortly, the difference is coming from the different expressions for pattern parameter of EF.Functions.Like , and the way LINQ to Objects (used by EF Core InMemory provider) processes IQueryable expression tree. EF.Functions.Like ,不同之处在于EF.Functions.Like pattern参数的不同表达式,以及 LINQ to Objects(由 EF Core InMemory 提供程序使用)处理IQueryable表达式树的方式。

First off, performance test with EF Core InMemory provider against small set of data is irrelevant, since it is measuring basically the query expression tree building, while in the case of real database, most of the time is executing the generated SQL query, returning and materializing the result data set.首先,使用 EF Core InMemory 提供程序针对小数据集进行性能测试是无关紧要的,因为它基本上测量的是查询表达式树的构建,而在真实数据库的情况下,大部分时间是执行生成的 SQL 查询,返回和物化结果数据集。

Second, regarding二、关于

I was worried that I would get a performance hit for using Expressions and a bit of reflection我担心我会因为使用表达式和一些反思而受到影响

Both approaches build query expression tree at runtime using Expression class methods.这两种方法都使用Expression类方法在运行时构建查询表达式树。 The only difference is that the C# compiler generates that code for you at compile time, thus has no reflection calls.唯一的区别是 C# 编译器在编译时为您生成该代码,因此没有反射调用。 But your code could easily be modified to avoid reflection as well, thus making the generation fully equivalent.但是您的代码也可以轻松修改以避免反射,从而使生成完全等效。

More important difference is that your code is emitting ConstantExpression , while currently C# compiler has no way to generate constant expressions from variables, so it always emits closures, which in turn are bound as query parameters by the EF Core query translator.更重要的区别是您的代码发出ConstantExpression ,而当前 C# 编译器无法从变量生成常量表达式,因此它总是发出闭包,而闭包又由 EF Core 查询转换器绑定为查询参数。 Which in general is recommended for SQL queries, so you'd better do the same inside your method, or have an option to do so.通常建议将其用于 SQL 查询,因此您最好在方法中执行相同的操作,或者可以选择这样做。

So, to recap shortly, your method binds constant expression, and compiler method binds closure.因此,简要回顾一下,您的方法绑定了常量表达式,而编译器方法绑定了闭包。 But not only.但不仅如此。 See here看这里

query.Where(x => EF.Functions.Like(x.Value, $"%{SearchValue1}%"))

SearchValue1 variable is converted to closure, but since $"%{SearchValue1}%" is part of expression , it's not evaluated at that point, but is recorded as MethodCallExpression to string.Format . SearchValue1变量被转换为闭包,但由于$"%{SearchValue1}%"expression 的一部分,因此它不会在此时计算,而是作为MethodCallExpression记录到string.Format

These two gives the big performance difference in LINQ to Objects, since it executes query expression trees by first compiling expressions to delegate(s), and then running it.这两者在 LINQ to Objects 中提供了很大的性能差异,因为它通过首先将表达式编译为委托,然后运行它来执行查询表达式树。 So at the end your code passes constant value, and compiler generated query code calls string.Format .所以最后你的代码传递常量值,编译器生成的查询代码调用string.Format And there is a big difference in compilation/execution time between the two.并且两者在编译/执行时间上存在很大差异。 Multiplied by 3 in your test.在您的测试中乘以 3。


With all that being said, let see it in action.说了这么多,让我们看看它的实际效果。

First, the optimized extension method with one-time static reflection info caching and option for using constant or variable:第一,优化的扩展方法,具有一次性静态反射信息缓存和使用常量或变量的选项:

public static IQueryable<TEntity> WhereIsLikeIfStringIsNotEmpty<TEntity>(
    this IQueryable<TEntity> query,
    string searchValue,
    Expression<Func<TEntity, string>> selector,
    bool useVariable = false)
{
    if (string.IsNullOrEmpty(searchValue)) return query;
    var parameter = selector.Parameters[0];
    var pattern = Value($"%{searchValue}%", useVariable);
    var body = Expression.Call(LikeMethod, DbFunctionsArg, selector.Body, pattern);
    var predicate = Expression.Lambda<Func<TEntity, bool>>(body, parameter);
    return query.Where(predicate);
}

static Expression Value(string value, bool variable)
{
    if (!variable) return Expression.Constant(value);
    return Expression.Property(
        Expression.Constant(new StringVar { Value = value }),
        StringVar.ValueProperty);
}

class StringVar
{
    public string Value { get; set; }
    public static PropertyInfo ValueProperty { get; } = typeof(StringVar).GetProperty(nameof(Value));
}

static Expression DbFunctionsArg { get; } = Expression.Constant(EF.Functions);

static MethodInfo LikeMethod { get; } = typeof(DbFunctionsExtensions).GetMethod(nameof(DbFunctionsExtensions.Like), new[]
{
    typeof(DbFunctions),
    typeof(string),
    typeof(string)
});

Note that I removed the Property from the method name and the requirement for MemberExpression , since it is not needed - the method will work with any string returning expression.请注意,我从方法名称中删除了PropertyMemberExpression的要求,因为它不是必需的 - 该方法将与任何string返回表达式一起使用。

Second, add two new benchmark methods for it:其次,为其添加两个新的基准测试方法:


[Benchmark]
public void FilterUsingExtensionOptimizedUsingConstant()
{
    var _ = _context.TestModels
        .WhereIsLikeIfStringIsNotEmpty(SearchValue1, testModel => testModel.Value, false)
        .WhereIsLikeIfStringIsNotEmpty(SearchValue2, testModel => testModel.OtherValue, false)
        .WhereIsLikeIfStringIsNotEmpty(SearchValue3, testModel => testModel.ThirdValue, false)
        .ToList();
}

[Benchmark]
public void FilterUsingExtensionOptimizedUsingVariable()
{
    var _ = _context.TestModels
        .WhereIsLikeIfStringIsNotEmpty(SearchValue1, testModel => testModel.Value, true)
        .WhereIsLikeIfStringIsNotEmpty(SearchValue2, testModel => testModel.OtherValue, true)
        .WhereIsLikeIfStringIsNotEmpty(SearchValue3, testModel => testModel.ThirdValue, true)
        .ToList();
}

Finally, add benchmark for optimized version of "traditional way" which avoids string.Format in the expression tree (but still binds variable):最后,为“传统方式”的优化版本添加基准测试,它避免了表达式树中的string.Format (但仍然绑定变量):

[Benchmark]
public void FilterTraditionallyOptimized()
{
    var query = _context.TestModels.AsQueryable();
    if (!string.IsNullOrEmpty(SearchValue1))
    {
        var pattern = $"%{SearchValue1}%";
        query = query.Where(x => EF.Functions.Like(x.Value, pattern));
    }
    if (!string.IsNullOrEmpty(SearchValue2))
    {
        var pattern = $"%{SearchValue2}%";
        query = query.Where(x => EF.Functions.Like(x.OtherValue, pattern));
    }
    if (!string.IsNullOrEmpty(SearchValue3))
    {
        var pattern = $"%{SearchValue3}%";
        query = query.Where(x => EF.Functions.Like(x.ThirdValue, pattern));
    }

    var _ = query.ToList();
}

The results:结果:

Method方法 Mean吝啬的 Error错误 StdDev标准差
FilterUsingExtension过滤器使用扩展 51.84 us 51.84 美元 0.089 us 0.089 美元 0.079 us 0.079 美元
FilterUsingExtensionOptimizedUsingConstant FilterUsingExtensionOptimizedUsingConstant 48.95 us 48.95 美元 0.061 us 0.061 美元 0.054 us 0.054 美元
FilterUsingExtensionOptimizedUsingVariable FilterUsingExtensionOptimizedUsingVariable 58.40 us 58.40 美元 0.354 us 0.354 美元 0.331 us 0.331 美元
FilterTraditionally传统过滤 625.40 us 625.40 美元 1.269 us 1.269 美元 1.187 us 1.187 美元
FilterTraditionallyOptimized过滤器传统优化 60.09 us 60.09 美元 0.491 us 0.491 美元 0.435 us 0.435 美元

As we can see, optimized extension method using constant is fastest, but very close to your original (which means the reflection is not essential).正如我们所看到的,使用常量的优化扩展方法最快,但非常接近您的原始方法(这意味着反射不是必需的)。

The variant with variable is bit slower, but generally will be better when used against real database.带变量的变体有点慢,但在用于真实数据库时通常会更好。

The optimized "traditional" method is bit slower than the previous two, which is kind of surprising, but the difference is negligible.优化后的“传统”方法比前两个慢一点,这有点令人惊讶,但差异可以忽略不计。

The original "traditional" method is a way slower than all previous, due to the aforementioned reasons.由于上述原因,原始的“传统”方法比以前所有方法都慢。 But against real database it would be negligible part of the overall query execution.但是对于真实的数据库,它在整个查询执行中可以忽略不计。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM