91 votes

C# Compilé Les Expressions Lambda Performance

Considérez les points suivants simple manipulation sur une collection:

static List<int> x = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var result = x.Where(i => i % 2 == 0).Where(i => i > 5);

Maintenant, nous allons utiliser des Expressions. Le code suivant est à peu près équivalent:

static void UsingLambda() {
    Func<IEnumerable<int>, IEnumerable<int>> lambda = l => l.Where(i => i % 2 == 0).Where(i => i > 5);
    var t0 = DateTime.Now.Ticks;
    for (int j = 1; j < MAX; j++) 
        var sss = lambda(x).ToList();

    var tn = DateTime.Now.Ticks;
    Console.WriteLine("Using lambda: {0}", tn - t0);
}

Mais je veux construire l'expression à la volée, donc voici un nouveau test:

static void UsingCompiledExpression() {
    var f1 = (Expression<Func<IEnumerable<int>, IEnumerable<int>>>)(l => l.Where(i => i % 2 == 0));
    var f2 = (Expression<Func<IEnumerable<int>, IEnumerable<int>>>)(l => l.Where(i => i > 5));
    var argX = Expression.Parameter(typeof(IEnumerable<int>), "x");
    var f3 = Expression.Invoke(f2, Expression.Invoke(f1, argX));
    var f = Expression.Lambda<Func<IEnumerable<int>, IEnumerable<int>>>(f3, argX);

    var c3 = f.Compile();

    var t0 = DateTime.Now.Ticks;
    for (int j = 1; j < MAX; j++) 
        var sss = c3(x).ToList();

    var tn = DateTime.Now.Ticks;
    Console.WriteLine("Using lambda compiled: {0}", tn - t0);
}

Bien sûr, il n'est pas exactement comme ci-dessus, donc pour être juste, je modifie le premier légèrement:

static void UsingLambdaCombined() {
    Func<IEnumerable<int>, IEnumerable<int>> f1 = l => l.Where(i => i % 2 == 0);
    Func<IEnumerable<int>, IEnumerable<int>> f2 = l => l.Where(i => i > 5);
    Func<IEnumerable<int>, IEnumerable<int>> lambdaCombined = l => f2(f1(l));
    var t0 = DateTime.Now.Ticks;
    for (int j = 1; j < MAX; j++) 
        var sss = lambdaCombined(x).ToList();

    var tn = DateTime.Now.Ticks;
    Console.WriteLine("Using lambda combined: {0}", tn - t0);
}

Vient maintenant les résultats pour MAX = 100000, VS2008, le débogage SUR:

Using lambda compiled: 23437500
Using lambda:           1250000
Using lambda combined:  1406250

Et avec le débogage OFF:

Using lambda compiled: 21718750
Using lambda:            937500
Using lambda combined:  1093750

Surprise. La compilation de l'expression est à peu près 17x plus lent que les autres solutions. Voici maintenant les questions:

  1. Suis-je en comparant les non-expressions équivalentes?
  2. Est-il un mécanisme pour faire .NET "optimiser" la compilation de l'expression?
  3. Comment puis-je exprimer la même chaîne appel l.Where(i => i % 2 == 0).Where(i => i > 5); par programmation?

Certains plus de statistiques. Visual Studio 2010, le débogage, les optimisations OFF:

Using lambda:           1093974
Using lambda compiled: 15315636
Using lambda combined:   781410

Débogage SUR, optimisations:

Using lambda:            781305
Using lambda compiled: 15469839
Using lambda combined:   468783

Débogage OFF, optimisations:

Using lambda:            625020
Using lambda compiled: 14687970
Using lambda combined:   468765

Nouvelle Surprise. Commutation de VS2008 (C 3), afin de VS2010 (C#4), fait de l' UsingLambdaCombined plus rapide que le natif de lambda.


Ok, j'ai trouvé un moyen d'améliorer le lambda compilé performance de plus d'un ordre de grandeur. Voici une astuce; après l'exécution de la profiler, 92% du temps est consacré à:

System.Reflection.Emit.DynamicMethod.CreateDelegate(class System.Type, object)

Hmmmm... Pourquoi est-il de la création d'un nouveau délégué à chaque itération? Je ne suis pas sûr, mais la solution suit dans un poste distinct.

43voto

Hugo S Ferreira Points 2314

Pourrait-il être à l'intérieur des lambdas ne sont pas compilés?!? Voici une preuve de concept:

static void UsingCompiledExpressionWithMethodCall() {
        var where = typeof(Enumerable).GetMember("Where").First() as System.Reflection.MethodInfo;
        where = where.MakeGenericMethod(typeof(int));
        var l = Expression.Parameter(typeof(IEnumerable<int>), "l");
        var arg0 = Expression.Parameter(typeof(int), "i");
        var lambda0 = Expression.Lambda<Func<int, bool>>(
            Expression.Equal(Expression.Modulo(arg0, Expression.Constant(2)),
                             Expression.Constant(0)), arg0).Compile();
        var c1 = Expression.Call(where, l, Expression.Constant(lambda0));
        var arg1 = Expression.Parameter(typeof(int), "i");
        var lambda1 = Expression.Lambda<Func<int, bool>>(Expression.GreaterThan(arg1, Expression.Constant(5)), arg1).Compile();
        var c2 = Expression.Call(where, c1, Expression.Constant(lambda1));

        var f = Expression.Lambda<Func<IEnumerable<int>, IEnumerable<int>>>(c2, l);

        var c3 = f.Compile();

        var t0 = DateTime.Now.Ticks;
        for (int j = 1; j < MAX; j++)
        {
            var sss = c3(x).ToList();
        }

        var tn = DateTime.Now.Ticks;
        Console.WriteLine("Using lambda compiled with MethodCall: {0}", tn - t0);
    }

Et maintenant, les horaires sont les suivants:

Using lambda:                            625020
Using lambda compiled:                 14687970
Using lambda combined:                   468765
Using lambda compiled with MethodCall:   468765

Woot! Non seulement il est rapide, il est plus rapide que le natif de lambda. (Elle se gratte la tête).


Bien sûr, le code ci-dessus est tout simplement trop douloureux à écrire. Faisons simple magie:

static void UsingCompiledConstantExpressions() {
    var f1 = (Func<IEnumerable<int>, IEnumerable<int>>)(l => l.Where(i => i % 2 == 0));
    var f2 = (Func<IEnumerable<int>, IEnumerable<int>>)(l => l.Where(i => i > 5));
    var argX = Expression.Parameter(typeof(IEnumerable<int>), "x");
    var f3 = Expression.Invoke(Expression.Constant(f2), Expression.Invoke(Expression.Constant(f1), argX));
    var f = Expression.Lambda<Func<IEnumerable<int>, IEnumerable<int>>>(f3, argX);

    var c3 = f.Compile();

    var t0 = DateTime.Now.Ticks;
    for (int j = 1; j < MAX; j++) {
        var sss = c3(x).ToList();
    }

    var tn = DateTime.Now.Ticks;
    Console.WriteLine("Using lambda compiled constant: {0}", tn - t0);
}

Et certains horaires, VS2010, Optimisations, le Débogage OFF:

Using lambda:                            781260
Using lambda compiled:                 14687970
Using lambda combined:                   468756
Using lambda compiled with MethodCall:   468756
Using lambda compiled constant:          468756

Maintenant, vous pourriez vous dire que je ne suis pas générer l'ensemble de l'expression de manière dynamique; le chaînage des appels. Mais dans l'exemple ci-dessus, je générer l'ensemble de l'expression. Et les horaires de match. C'est juste un raccourci pour écrire moins de code.


De ma compréhension, ce qui se passe est que le .Compiler() la méthode ne propage pas les compilations à l'intérieur des lambdas, et donc l'invocation constante de CreateDelegate. Mais pour comprendre vraiment, j'aimerais avoir un .NET le gourou de commentaire un peu sur l'intérieur des trucs en cours.

Et pourquoi, oh pourquoi est-ce maintenant plus rapide qu'un natif lambda!?

10voto

JulianR Points 7257

J'ai demandé récemment à peu près la même question:

La Performance de compilé-à-délégué Expression

La solution a été pour moi que je ne devrais pas appeler à l' Compile sur le Expression, mais que je devrais appeler CompileToMethod sur il et de compiler l' Expression d'un static méthode dans une dynamique de l'assemblage.

Comme suit:

var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(
  new AssemblyName("MyAssembly_" + Guid.NewGuid().ToString("N")), 
  AssemblyBuilderAccess.Run);

var moduleBuilder = assemblyBuilder.DefineDynamicModule("Module");

var typeBuilder = moduleBuilder.DefineType("MyType_" + Guid.NewGuid().ToString("N"), 
  TypeAttributes.Public));

var methodBuilder = typeBuilder.DefineMethod("MyMethod", 
  MethodAttributes.Public | MethodAttributes.Static);

expression.CompileToMethod(methodBuilder);

var resultingType = typeBuilder.CreateType();

var function = Delegate.CreateDelegate(expression.Type,
  resultingType.GetMethod("MyMethod"));

Ce n'est pas idéal. Je ne suis pas tout à fait certain de types, ce qui s'applique exactement, mais je pense que les types qui sont pris comme paramètres par le délégué, ou retourné par le délégué avoir à être public et non générique. Il doit être non-générique parce que les types génériques apparemment accéder System.__Canon qui est un type interne utilisé par .NET sous le capot pour les types génériques et cela est une violation de la "a à un public type de règle).

Pour ces types, vous pouvez utiliser le plus lent, apparemment Compile. Je détecte de la façon suivante:

private static bool IsPublicType(Type t)
{

  if ((!t.IsPublic && !t.IsNestedPublic) || t.IsGenericType)
  {
    return false;
  }

  int lastIndex = t.FullName.LastIndexOf('+');

  if (lastIndex > 0)
  {
    var containgTypeName = t.FullName.Substring(0, lastIndex);

    var containingType = Type.GetType(containgTypeName + "," + t.Assembly);

    if (containingType != null)
    {
      return containingType.IsPublic;
    }

    return false;
  }
  else
  {
    return t.IsPublic;
  }
}

Mais comme je l'ai dit, ce n'est pas l'idéal et je voudrais quand même savoir pourquoi la compilation d'une méthode à une dynamique de l'assemblage est parfois un ordre de grandeur plus rapide. Et je le dis parfois, parce que j'ai aussi vu des cas où un Expression compilé avec Compile est tout aussi rapide comme une méthode normale. Voir ma question.

Ou si quelqu'un connait un moyen de contourner le "non-public" types de contrainte avec la dynamique de l'assemblage, c'est la bienvenue.

4voto

Jeff Mercado Points 42075

Vos expressions ne sont pas équivalentes et donc vous obtenez des résultats faussés. J'ai écrit un banc d'essai pour tester cela. Les tests réguliers lambda appel, l'équivalent compilé expression, une main équivalent compilé expression, ainsi que des composés versions. Ceux-ci devraient être plus des chiffres exacts. Il est intéressant, je ne vois pas beaucoup de variations entre la plaine et composé des versions. Et la compilation des expressions sont naturellement plus lent mais que par très peu. Vous avez besoin d'un assez grand d'entrée et le nombre d'itérations pour obtenir quelques bons numéros. Cela fait une différence.

Quant à votre deuxième question, je ne sais pas comment vous seriez en mesure d'obtenir plus de performance de ce donc je ne peux pas vous aider là-bas. Il semble aussi bon que ça va arriver.

Vous trouverez ma réponse à votre troisième question dans l' HandMadeLambdaExpression() méthode. Pas la méthode la plus simple expression de construire en raison de l'extension des méthodes, mais c'est faisable.

using System;
using System.Collections.Generic;
using System.Linq;

using System.Diagnostics;
using System.Linq.Expressions;

namespace ExpressionBench
{
    class Program
    {
        static void Main(string[] args)
        {
            var values = Enumerable.Range(0, 5000);
            var lambda = GetLambda();
            var lambdaExpression = GetLambdaExpression().Compile();
            var handMadeLambdaExpression = GetHandMadeLambdaExpression().Compile();
            var composed = GetComposed();
            var composedExpression = GetComposedExpression().Compile();
            var handMadeComposedExpression = GetHandMadeComposedExpression().Compile();

            DoTest("Lambda", values, lambda);
            DoTest("Lambda Expression", values, lambdaExpression);
            DoTest("Hand Made Lambda Expression", values, handMadeLambdaExpression);
            Console.WriteLine();
            DoTest("Composed", values, composed);
            DoTest("Composed Expression", values, composedExpression);
            DoTest("Hand Made Composed Expression", values, handMadeComposedExpression);
        }

        static void DoTest<TInput, TOutput>(string name, TInput sequence, Func<TInput, TOutput> operation, int count = 1000000)
        {
            for (int _ = 0; _ < 1000; _++)
                operation(sequence);
            var sw = Stopwatch.StartNew();
            for (int _ = 0; _ < count; _++)
                operation(sequence);
            sw.Stop();
            Console.WriteLine("{0}:", name);
            Console.WriteLine("  Elapsed: {0,10} {1,10} (ms)", sw.ElapsedTicks, sw.ElapsedMilliseconds);
            Console.WriteLine("  Average: {0,10} {1,10} (ms)", decimal.Divide(sw.ElapsedTicks, count), decimal.Divide(sw.ElapsedMilliseconds, count));
        }

        static Func<IEnumerable<int>, IList<int>> GetLambda()
        {
            return v => v.Where(i => i % 2 == 0).Where(i => i > 5).ToList();
        }

        static Expression<Func<IEnumerable<int>, IList<int>>> GetLambdaExpression()
        {
            return v => v.Where(i => i % 2 == 0).Where(i => i > 5).ToList();
        }

        static Expression<Func<IEnumerable<int>, IList<int>>> GetHandMadeLambdaExpression()
        {
            var enumerableMethods = typeof(Enumerable).GetMethods();
            var whereMethod = enumerableMethods
                .Where(m => m.Name == "Where")
                .Select(m => m.MakeGenericMethod(typeof(int)))
                .Where(m => m.GetParameters()[1].ParameterType == typeof(Func<int, bool>))
                .Single();
            var toListMethod = enumerableMethods
                .Where(m => m.Name == "ToList")
                .Select(m => m.MakeGenericMethod(typeof(int)))
                .Single();

            // helpers to create the static method call expressions
            Func<Expression, ParameterExpression, Func<ParameterExpression, Expression>, Expression> WhereExpression =
                (instance, param, body) => Expression.Call(whereMethod, instance, Expression.Lambda(body(param), param));
            Func<Expression, Expression> ToListExpression =
                instance => Expression.Call(toListMethod, instance);

            //return v => v.Where(i => i % 2 == 0).Where(i => i > 5).ToList();
            var exprParam = Expression.Parameter(typeof(IEnumerable<int>), "v");
            var expr0 = WhereExpression(exprParam,
                Expression.Parameter(typeof(int), "i"),
                i => Expression.Equal(Expression.Modulo(i, Expression.Constant(2)), Expression.Constant(0)));
            var expr1 = WhereExpression(expr0,
                Expression.Parameter(typeof(int), "i"),
                i => Expression.GreaterThan(i, Expression.Constant(5)));
            var exprBody = ToListExpression(expr1);
            return Expression.Lambda<Func<IEnumerable<int>, IList<int>>>(exprBody, exprParam);
        }

        static Func<IEnumerable<int>, IList<int>> GetComposed()
        {
            Func<IEnumerable<int>, IEnumerable<int>> composed0 =
                v => v.Where(i => i % 2 == 0);
            Func<IEnumerable<int>, IEnumerable<int>> composed1 =
                v => v.Where(i => i > 5);
            Func<IEnumerable<int>, IList<int>> composed2 =
                v => v.ToList();
            return v => composed2(composed1(composed0(v)));
        }

        static Expression<Func<IEnumerable<int>, IList<int>>> GetComposedExpression()
        {
            Expression<Func<IEnumerable<int>, IEnumerable<int>>> composed0 =
                v => v.Where(i => i % 2 == 0);
            Expression<Func<IEnumerable<int>, IEnumerable<int>>> composed1 =
                v => v.Where(i => i > 5);
            Expression<Func<IEnumerable<int>, IList<int>>> composed2 =
                v => v.ToList();
            var exprParam = Expression.Parameter(typeof(IEnumerable<int>), "v");
            var exprBody = Expression.Invoke(composed2, Expression.Invoke(composed1, Expression.Invoke(composed0, exprParam)));
            return Expression.Lambda<Func<IEnumerable<int>, IList<int>>>(exprBody, exprParam);
        }

        static Expression<Func<IEnumerable<int>, IList<int>>> GetHandMadeComposedExpression()
        {
            var enumerableMethods = typeof(Enumerable).GetMethods();
            var whereMethod = enumerableMethods
                .Where(m => m.Name == "Where")
                .Select(m => m.MakeGenericMethod(typeof(int)))
                .Where(m => m.GetParameters()[1].ParameterType == typeof(Func<int, bool>))
                .Single();
            var toListMethod = enumerableMethods
                .Where(m => m.Name == "ToList")
                .Select(m => m.MakeGenericMethod(typeof(int)))
                .Single();

            Func<ParameterExpression, Func<ParameterExpression, Expression>, Expression> LambdaExpression =
                (param, body) => Expression.Lambda(body(param), param);
            Func<Expression, ParameterExpression, Func<ParameterExpression, Expression>, Expression> WhereExpression =
                (instance, param, body) => Expression.Call(whereMethod, instance, Expression.Lambda(body(param), param));
            Func<Expression, Expression> ToListExpression =
                instance => Expression.Call(toListMethod, instance);

            var composed0 = LambdaExpression(Expression.Parameter(typeof(IEnumerable<int>), "v"),
                v => WhereExpression(
                    v,
                    Expression.Parameter(typeof(int), "i"),
                    i => Expression.Equal(Expression.Modulo(i, Expression.Constant(2)), Expression.Constant(0))));
            var composed1 = LambdaExpression(Expression.Parameter(typeof(IEnumerable<int>), "v"),
                v => WhereExpression(
                    v,
                    Expression.Parameter(typeof(int), "i"),
                    i => Expression.GreaterThan(i, Expression.Constant(5))));
            var composed2 = LambdaExpression(Expression.Parameter(typeof(IEnumerable<int>), "v"),
                v => ToListExpression(v));

            var exprParam = Expression.Parameter(typeof(IEnumerable<int>), "v");
            var exprBody = Expression.Invoke(composed2, Expression.Invoke(composed1, Expression.Invoke(composed0, exprParam)));
            return Expression.Lambda<Func<IEnumerable<int>, IList<int>>>(exprBody, exprParam);
        }
    }
}

Et les résultats sur ma machine:

Lambda:
 Écoulé: 340971948 123230 (ms)
 Moyenne: 340.971948 0.12323 (ms)
Expression Lambda:
 Écoulé: 357077202 129051 (ms)
 Moyenne: 357.077202 0.129051 (ms)
Fabriqué À La Main Lambda Expression:
 Écoulé: 345029281 124696 (ms)
 Moyenne: 345.029281 0.124696 (ms)

Composé:
 Écoulé: 340409238 123027 (ms)
 Moyenne: 340.409238 0.123027 (ms)
Composé D'Expression:
 Écoulé: 350800599 126782 (ms)
 Moyenne: 350.800599 0.126782 (ms)
Faite À La Main Composée D'Expression:
 Écoulé: 352811359 127509 (ms)
 Moyenne: 352.811359 0.127509 (ms)

3voto

Akash Kava Points 18026

Compilé lambda performance sur les délégués peuvent être plus lente, car le code Compilé au moment de l'exécution ne peut pas être optimisé toutefois, le code que vous avez écrit manuellement et qui a compilé via le compilateur C# est optimisé.

Deuxièmement, plusieurs expressions lambda-à-dire plusieurs méthodes anonymes, et appelle chacun d'entre eux prend peu de temps supplémentaire, au cours de l'évaluation d'un droit de méthode. Par exemple, l'appel

Console.WriteLine(x);

et

Action x => Console.WriteLine(x);
x(); // this means two different calls..

sont différents, et la deuxième avec un peu plus de surcharge est nécessaire qu'à partir du compilateur point de vue, sa fait deux appels différents. Le premier appel x lui-même, puis à l'intérieur qu'en appelant x la déclaration.

Si votre combiné Lambda aura certainement un peu de ralentissement des performances de plus simple expression lambda.

Et ceci est indépendant de ce qui est en cours d'exécution à l'intérieur, parce que vous êtes encore en phase d'évaluation correcte de la logique, mais vous êtes l'ajout d'étapes supplémentaires pour le compilateur d'effectuer.

Même après l'expression de l'arbre est compilé, il n'aura pas d'optimisation, et il sera toujours préserver son peu de structure complexe, l'évaluation et l'appelant peut avoir une validation supplémentaire, null check, etc, qui pourraient être en train de ralentir les performances de compilé les expressions lambda.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by: