2017-01-25 1 views
3

Je suis un grand fan de la tendance relativement récente d'utiliser des expressions lambda au lieu de chaînes pour indiquer des propriétés dans, par exemple, le mappage ORM. Fortement typé >>>> Stringly typé.Suggestions pour optimiser les expressions de passage en tant que paramètres de méthode

Pour être clair, voici ce dont je parle:

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"); 

Dans certains travaux récents, je l'ai fait, j'ai besoin de trucs de mise en cache basée sur l'expression et de le faire, J'avais besoin de créer une clé basée sur l'expression. Comme ceci:

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; 
} 

Remarques: La version de StringBuilder est presque identique. Il est seulement supposé fonctionner pour les expressions qui ont la forme x => x.A.B.C, toute autre chose est une erreur et devrait échouer. Oui, je dois mettre en cache. Non, la compilation est beaucoup plus lente que la génération/comparaison de clés dans mon cas. En comparant diverses fonctions de keygen, j'ai été mystifié de découvrir qu'elles se comportaient toutes horriblement.
Même la version fictive qui vient de retourner "". Après quelques tentatives, j'ai découvert que c'était vraiment l'instanciation de l'objet Expression qui était super chère.

Voici la sortie de la nouvelle référence que j'ai créé pour mesurer cet effet:

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 

Et voici le code de la référence:

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

Cela démontre clairement que speedup 2000 -10000 fois peut être atteint avec une mise en cache simple.

Le problème est, que ces solutions de contournement, à divers degrés, compromettent la beauté et la sécurité de l'utilisation des expressions de cette manière.

La deuxième solution de contournement au moins maintient la ligne d'expression, mais il est loin d'être assez,

Ainsi, les questions se sont-il d'autres solutions de contournement que je pourrais avoir manqué, qui sont moins laids?

Merci à l'avance

+0

Eh bien, la création d'un 'Expression' d'une expression lambda n'est pas libre, est toujours va être apreciably plus cher que d'avoir la' Expression' pour commencer. La solution est évidemment, comme vous le savez déjà, la mise en cache. Il n'y a aucun moyen de contourner cela à moins de pouvoir créer un analyseur d'expression lambda plus rapide ... – InBetween

+0

Peut-être existe-t-il un moyen de spécifier des expressions lambda pour que le compilateur puisse optimiser et réutiliser la même instance? Je suis à la recherche de solutions de rechange alternatives. S'il n'y en a pas, alors ça va et je vais simplement utiliser la mise en cache sur les chemins d'accès à chaud. En C++, je regarderais les statistiques et les macros locales en ce moment. – JJJ

+0

Intéressant. Les délégués lambda statiques (comme 'Func <...>' etc.) sont mis en cache, je considérais automatiquement la même chose pour 'Expression >', mais vous avez raison, ils sont non seulement instanciés, mais aussi construits avec des méthodes 'Expression' à tout moment. appel. On dirait qu'il n'y a pas d'autre moyen actuellement de les mettre en cache dans des champs statiques en lecture seule. –

Répondre

0

Après avoir réfléchi sur la mise en cache statique des propriétés pendant un certain temps je suis venu avec ceci:

Dans ce cas particulier toutes les expressions de propriété, je me suis intéressé à été sur les entités simples POCO DB . J'ai donc décidé de rendre ces classes partielles et d'ajouter les propriétés de cache statique dans une autre classe de paire partielle. Ayant vu que cela fonctionnait, j'ai décidé d'essayer de l'automatiser. J'ai regardé T4, mais cela n'a pas semblé approprié à cet effet. Au lieu de cela, j'ai essayé https://github.com/daveaglick/Scripty, ce qui est assez génial.

Voici le script que j'utilise pour générer mes cours de mise en cache:

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([email protected]"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(@" } 
}"); 
    } 

} 

Une classe d'entrée pourrait ressembler à ceci:

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

Le script génère ensuite un fichier nommé SomeClassExprs.cs, avec le contenu suivant:

using System; 
using System.Linq.Expressions; 

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

Les fichiers sont générés dans un dossier appelé codegen, que je ex clude du contrôle de la source. Scripty veille à inclure les fichiers lors de la compilation.

Dans l'ensemble, je suis très heureux de cette approche.

:)