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

Suggestions for optimizing passing expressions as method parameters

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

明确地说,这就是我所说的:

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 形式的表达式,任何其他都是错误并且应该失败。是的,我需要缓存。不,在我的例子中,编译比 key generation/comparison 慢得多。

在对各种注册机功能进行基准测试时,我很困惑地发现它们的性能都非常糟糕。
即使是刚刚返回的虚拟版本 "".

经过一些摆弄,我发现实际上是 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 倍的加速。

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

第二个解决方法至少使表达式保持内联,但它远非漂亮,

所以问题是,是否还有我可能错过的其他变通方法,这些变通方法不那么难看?

提前致谢

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

在这种特殊情况下,我感兴趣的所有 属性 表达式都在简单的 POCO 数据库实体上。所以我决定将这些 classes 部分化,并在另一个部分对 class.

中添加静态缓存属性

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

这是我用来生成缓存的脚本 classes:

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( @"    }
}" );
    }

}

输入 class 可能如下所示:

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 确保在编译期间包含这些文件。

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

:)