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

时间:2017-01-25 09:23:42

标签: c# lambda benchmarking

我非常喜欢使用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形式的表达式,其他任何错误都应该失败。是的我需要缓存。不,在我的情况下,编译比密钥生成/比较慢得多。

在对各种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倍的加速。

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

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

所以问题是,是否还有其他一些我可能错过的解决方法,哪些不那么难看?

提前致谢

1 个答案:

答案 0 :(得分:0)

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

在这种特殊情况下,我感兴趣的所有属性表达式都是在简单的POCO DB实体上。所以我决定使这些类成为局部,并在另一个部分对类中添加静态缓存属性。

看到这个工作,我决定尝试自动化它。我看了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确保在编译期间包含文件。

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

:)