I don't like current spec evaluation because it compiles expressions with every call to Evaluate.
So i have added benchmark project to provide ability to measure performance and replaced IEnumerable<(Expression<Func<T, string>>, string, int)> ISpecification{T}.SearchCriterias with IEnumerable<SearchExpressionBase<T>>. There also 2 approaches to compile expressions: one is default dotnet compile and second is CompileFast from dadhi/FastExpressionCompiler.
With this changes user code could cache specifications to not just reduce amount of allocations but also to avoid recompilation.
I'm not sure if it's super precise benchmark since i don't have enough experience in such things and i've ran it on my home PC, however everyone can do it by himself to verify results.
I need to know, should i continue and do the same for other expressions or not?
Long story short, here's the code and benchmark results
publicinterfaceISpecification<T>{// other members omitted for brevityIEnumerable<SearchExpressionBase<T>>SearchCriterias{get;}}publicabstractclassSearchExpressionBase<T>{protectedSearchExpressionBase(Expression<Func<T,string>>source,stringsearchTerm,intsearchGroup=1){this.Source=source;this.SearchTerm=searchTerm;this.SearchGroup=searchGroup;}publicExpression<Func<T,string>>Source{get;}publicstringSearchTerm{get;}publicintSearchGroup{get;}publicabstractFunc<T,string>SourceFunc{get;}}publicsealedclassSearchExpression<T>:SearchExpressionBase<T>{privatereadonlyLazy<Func<T,string>>sourceFuncLazy;publicSearchExpression(Expression<Func<T,string>>source,stringsearchTerm,intsearchGroup=1):base(source,searchTerm,searchGroup){this.sourceFuncLazy=newLazy<Func<T,string>>(this.Source.Compile);}publicoverrideFunc<T,string>SourceFunc=>this.sourceFuncLazy.Value;}publicsealedclassSearchExpressionFast<T>:SearchExpressionBase<T>{privatereadonlyLazy<Func<T,string>>sourceFuncLazy;publicSearchExpressionFast(Expression<Func<T,string>>source,stringsearchTerm,intsearchGroup=1):base(source,searchTerm,searchGroup){this.sourceFuncLazy=newLazy<Func<T,string>>(()=>this.Source.CompileFast());}publicoverrideFunc<T,string>SourceFunc=>this.sourceFuncLazy.Value;}publicstaticISpecificationBuilder<T>Search<T>(thisISpecificationBuilder<T>specificationBuilder,Expression<Func<T,string>>selector,stringsearchTerm,intsearchGroup=1)whereT:class{((List<SearchExpressionBase<T>>)specificationBuilder.Specification.SearchCriterias).Add(newSearchExpression<T>(selector,searchTerm,searchGroup));returnspecificationBuilder;}publicstaticISpecificationBuilder<T>SearchFast<T>(thisISpecificationBuilder<T>specificationBuilder,Expression<Func<T,string>>selector,stringsearchTerm,intsearchGroup=1)whereT:class{((List<SearchExpressionBase<T>>)specificationBuilder.Specification.SearchCriterias).Add(newSearchExpressionFast<T>(selector,searchTerm,searchGroup));returnspecificationBuilder;}publicclassSearchEvaluator:IInMemoryEvaluator{privateSearchEvaluator(){}publicstaticSearchEvaluatorInstance{get;}=newSearchEvaluator();publicIEnumerable<T>Evaluate<T>(IEnumerable<T>query,ISpecification<T>specification){foreach(varsearchGroupinspecification.SearchCriterias.GroupBy(x =>x.SearchGroup)){varcriterias=searchGroup.Select(x =>(x.SourceFunc,x.SearchTerm));query=query.Where(x =>criterias.Any(c =>c.SourceFunc(x).Like(c.SearchTerm)));}returnquery;}}[MemoryDiagnoser,MedianColumn,RankColumn,CsvExporter]publicclassInMemorySearchEvaluatorBenchmark{privateSearchEvaluatorevaluator;privateConsumerconsumer;privateIEnumerable<string>data;privateTestSpecificationspecification;privateTestSpecificationFastspecificationFast;[Params(1,10,100,1000)]publicintRepeatCount;[GlobalSetup]publicvoidGlobalSetup(){this.evaluator=SearchEvaluator.Instance;this.data=Enumerable.Range(1,247).Select(x =>$"Test {x%124} data.");this.specification=newTestSpecification();this.specificationFast=newTestSpecificationFast();this.consumer=newConsumer();}[Benchmark]publicvoidInMemorySearchEvaluator_Evaluate(){for(vari=0;i<this.RepeatCount;++i){this.evaluator.Evaluate(this.data,this.specification).Consume(this.consumer);}}[Benchmark]publicvoidInMemorySearchEvaluator_EvaluateFast(){for(vari=0;i<this.RepeatCount;++i){this.evaluator.Evaluate(this.data,this.specificationFast).Consume(this.consumer);}}privatesealedclassTestSpecification:Specification<string>{publicTestSpecification(){this.Query.Search(x =>x,"%123%");}}privatesealedclassTestSpecificationFast:Specification<string>{publicTestSpecificationFast(){this.Query.SearchFast(x =>x,"%123%");}}}BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19042.1165 (20H2/October2020Update)
Intel Core i5-9600K CPU 3.70GHz (Coffee Lake), 1 CPU, 6 logical and 6 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
Before
| Method | RepeatCount | Mean | Error | StdDev | Median | Rank | Gen 0 | Gen 1 | Gen 2 | Allocated |
|---|
| InMemorySearchEvaluator_Evaluate | 1 | 7.258 ms | 0.1064 ms | 0.0943 ms | 7.246 ms | 1 | 226.5625 | 109.3750 | 7.8125 | 1 MB |
| InMemorySearchEvaluator_Evaluate | 10 | 72.422 ms | 1.0351 ms | 0.9175 ms | 72.293 ms | 2 | 2285.7143 | 1142.8571 | - | 10 MB |
| InMemorySearchEvaluator_Evaluate | 100 | 738.127 ms | 14.2779 ms | 16.9969 ms | 732.809 ms | 3 | 23000.0000 | 11000.0000 | - | 105 MB |
| InMemorySearchEvaluator_Evaluate | 1000 | 7,304.372 ms | 69.1068 ms | 64.6425 ms | 7,320.962 ms | 4 | 234000.0000 | 117000.0000 | 10000.0000 | 1,049 MB |
After
| Method | RepeatCount | Mean | Error | StdDev | Median | Rank | Gen 0 | Allocated |
|---|
| InMemorySearchEvaluator_Evaluate | 1 | 104.2 μs | 0.92 μs | 0.77 μs | 103.7 μs | 1 | 14.4043 | 67 KB |
| InMemorySearchEvaluator_EvaluateFast | 1 | 103.8 μs | 0.51 μs | 0.48 μs | 103.7 μs | 1 | 14.4043 | 67 KB |
| InMemorySearchEvaluator_Evaluate | 10 | 1,015.9 μs | 12.42 μs | 11.62 μs | 1,016.4 μs | 2 | 144.5313 | 665 KB |
| InMemorySearchEvaluator_EvaluateFast | 10 | 1,035.8 μs | 16.52 μs | 15.45 μs | 1,037.6 μs | 3 | 144.5313 | 665 KB |
| InMemorySearchEvaluator_Evaluate | 100 | 10,844.6 μs | 215.23 μs | 376.97 μs | 10,984.3 μs | 5 | 1437.5000 | 6,651 KB |
| InMemorySearchEvaluator_EvaluateFast | 100 | 10,265.1 μs | 121.38 μs | 113.54 μs | 10,246.4 μs | 4 | 1437.5000 | 6,651 KB |
| InMemorySearchEvaluator_Evaluate | 1000 | 103,440.0 μs | 1,181.59 μs | 1,105.26 μs | 103,007.3 μs | 6 | 14400.0000 | 66,509 KB |
| InMemorySearchEvaluator_EvaluateFast | 1000 | 110,561.6 μs | 840.64 μs | 786.33 μs | 110,465.3 μs | 7 | 14400.0000 | 66,509 KB |
I don't like current spec evaluation because it compiles expressions with every call to
Evaluate.So i have added benchmark project to provide ability to measure performance and replaced
IEnumerable<(Expression<Func<T, string>>, string, int)> ISpecification{T}.SearchCriteriaswithIEnumerable<SearchExpressionBase<T>>. There also 2 approaches to compile expressions: one is default dotnet compile and second is CompileFast from dadhi/FastExpressionCompiler.With this changes user code could cache specifications to not just reduce amount of allocations but also to avoid recompilation.
I'm not sure if it's super precise benchmark since i don't have enough experience in such things and i've ran it on my home PC, however everyone can do it by himself to verify results.
I need to know, should i continue and do the same for other expressions or not?
Long story short, here's the code and benchmark results
Before
After