优化传递表达式作为方法参数的建议



我非常喜欢最近的趋势,即在ORM映射中使用lambda表达式而不是字符串来指示属性。强类型>>严格类型。

需要明确的是,这就是我所说的:

builder.Entity<WebserviceAccount>()
.HasTableName( "webservice_accounts" )
.HasPrimaryKey( _ => _.Id )
.Property( _ => _.Id ).HasColumnName( "id" )
.Property( _ => _.Username ).HasColumnName( "Username" ).HasLength( 255 )
.Property( _ => _.Password ).HasColumnName( "Password" ).HasLength( 255 )
.Property( _ => _.Active ).HasColumnName( "Active" );

在我最近做的一些工作中,我需要基于表达式缓存内容,为此,我需要根据表达式创建一个键。像这样:

static string GetExprKey( Expression<Func<Bar,int>> expr )
{
string key = "";
Expression e = expr.Body;
while( e.NodeType == ExpressionType.MemberAccess )
{
var me = (MemberExpression)e;
key += "<" + (me.Member as PropertyInfo).Name;
e = me.Expression;
}
key += ":" + ((ParameterExpression)e).Type.Name;
return key;
}

注意:StringBuilder版本的性能几乎相同。它只适用于形式为x => x.A.B.C的表达式,其他任何操作都是错误的,应该会失败。是的,我需要缓存。不,在我的情况下,编译比密钥生成/比较慢得多。

在对各种keygen功能进行基准测试时,我惊讶地发现它们的表现都很糟糕
甚至是刚刚返回""的伪版本。

经过一番折腾,我发现实际上是Expression对象的实例化非常昂贵。

以下是我为衡量这种影响而创建的新基准的输出:

Dummy( _ => _.F.Val ) 4106,5036 ms, 0,0041065036 ms/iter
Dummy( cachedExpr ) 0,3599 ms, 3,599E-07 ms/iter
Dummy( Bar_Foo_Val ?? (Bar_Foo_Val = _ => _.F.Val) ) 2,3127 ms, 2,3127E-06 ms/iter

这是基准测试的代码:

using System;
using System.Diagnostics;
using System.Linq.Expressions;
namespace ExprBench
{
sealed class Foo
{
public int Val { get; set; }
}
sealed class Bar
{
public Foo F { get; set; }
}

public static class ExprBench
{
static string Dummy( Expression<Func<Bar, int>> expr )
{
return "";
}
static Expression<Func<Bar, int>> Bar_Foo_Val;
static public void Run()
{
var sw = Stopwatch.StartNew();
TimeSpan elapsed;
int iterationCount = 1000000;
sw.Restart();
for( int j = 0; j<iterationCount; ++j )
Dummy( _ => _.F.Val );
elapsed = sw.Elapsed;
Console.WriteLine( $"Dummy( _ => _.F.Val ) {elapsed.TotalMilliseconds} ms, {elapsed.TotalMilliseconds/iterationCount} ms/iter" );
Expression<Func<Bar, int>> cachedExpr = _ => _.F.Val;
sw.Restart();
for( int j = 0; j<iterationCount; ++j )
Dummy( cachedExpr );
elapsed = sw.Elapsed;
Console.WriteLine( $"Dummy( cachedExpr ) {elapsed.TotalMilliseconds} ms, {elapsed.TotalMilliseconds/iterationCount} ms/iter" );
sw.Restart();
for( int j = 0; j<iterationCount; ++j )
Dummy( Bar_Foo_Val ?? (Bar_Foo_Val = _ => _.F.Val) );
elapsed = sw.Elapsed;
Console.WriteLine( $"Dummy( Bar_Foo_Val ?? (Bar_Foo_Val = _ => _.F.Val) ) {elapsed.TotalMilliseconds} ms, {elapsed.TotalMilliseconds/iterationCount} ms/iter" );
}
}
}

这清楚地表明,通过一些简单的缓存可以实现2000-10000次的加速。

问题是,这些变通方法在不同程度上损害了以这种方式使用表达式的美观性和安全性。

第二种解决方法至少保持表达式内联,但它远非完美,

所以问题是,我可能错过了其他不那么丑陋的解决方案吗?

提前感谢

考虑了一段时间属性的静态缓存后,我想到了这个:

在这个特殊的例子中,我感兴趣的所有属性表达式都是在简单的POCODB实体上。因此,我决定将这些类设为分部类,并在另一个分部对类中添加静态缓存属性。

看到这一点后,我决定尝试将其自动化。我查看了T4,但它似乎不适合这个目的。相反,我尝试了https://github.com/daveaglick/Scripty,非常棒。

以下是我用来生成缓存类的脚本:

using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Scripty.Core;
using System.Linq;
using System.Threading.Tasks;
bool IsInternalOrPublicSetter( AccessorDeclarationSyntax a )
{
return a.Kind() == SyntaxKind.SetAccessorDeclaration &&
a.Modifiers.Any( m => m.Kind() == SyntaxKind.PublicKeyword || m.Kind() == SyntaxKind.InternalKeyword );
}

foreach( var document in Context.Project.Analysis.Documents )
{
// Get all partial classes that inherit from IIsUpdatable
var allClasses = (await document.GetSyntaxRootAsync())
.DescendantNodes().OfType<ClassDeclarationSyntax>()
.Where( cls => cls.BaseList?.ChildNodes()?.SelectMany( _ => _.ChildNodes()?.OfType<IdentifierNameSyntax>() ).Select( id => id.Identifier.Text ).Contains( "IIsUpdatable" ) ?? false)
.Where( cls => cls.Modifiers.Any( m => m.ValueText == "partial" ))
.ToList();

foreach( var cls in allClasses )
{
var curFile = $"{cls.Identifier}Exprs.cs";
Output[curFile].WriteLine( $@"using System;
using System.Linq.Expressions;
namespace SomeNS
{{
public partial class {cls.Identifier}
{{" );
// Get all properties with public or internal setter
var props = cls.Members.OfType<PropertyDeclarationSyntax>().Where( prop => prop.AccessorList.Accessors.Any( IsInternalOrPublicSetter ) );
foreach( var prop in props )
{
Output[curFile].WriteLine( $"        public static Expression<Func<{cls.Identifier},object>> {prop.Identifier}Expr = _ => _.{prop.Identifier};" );
}
Output[curFile].WriteLine( @"    }
}" );
}
}

一个输入类可能看起来像这样:

public partial class SomeClass
{
public string Foo { get; internal set; }
}

然后,脚本生成一个名为SomeClassExprs.cs的文件,其中包含以下内容:

using System;
using System.Linq.Expressions;
namespace SomeNS
{
public partial class SomeClassExprs
{
public static Expression<Func<SomeClass,object>> FooExpr = _ => _.Foo;
}
}

这些文件是在一个名为codegen的文件夹中生成的,我将其从源代码管理中排除。

Scripty确保在编译过程中包含这些文件。

总而言之,我对这种方法很满意。

:)

相关内容

  • 没有找到相关文章

最新更新