Kompilierte C # Lambda Expressions Leistung

c# expression-trees lambda performance

Frage

Betrachten Sie die folgende einfache Manipulation einer Sammlung:

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

Jetzt verwenden wir Ausdrücke. Der folgende Code ist ungefähr gleichwertig:

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

Aber ich möchte den Ausdruck im Handumdrehen erstellen, also hier ist ein neuer 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);
}

Natürlich ist es nicht genau wie oben, also um fair zu sein, modifiziere ich das erste leicht:

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

Jetzt kommen die Ergebnisse für MAX = 100000, VS2008, Debugging ON:

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

Und mit Debugging OFF:

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

Überraschung . Der kompilierte Ausdruck ist ungefähr 17x langsamer als die anderen Alternativen. Jetzt kommen hier die Fragen:

  1. Vergleiche ich nicht äquivalente Ausdrücke?
  2. Gibt es einen Mechanismus, mit dem .NET den kompilierten Ausdruck "optimieren" kann?
  3. Wie l.Where(i => i % 2 == 0).Where(i => i > 5); ich den gleichen l.Where(i => i % 2 == 0).Where(i => i > 5); programmatisch?

Noch ein paar Statistiken. Visual Studio 2010, debuggen ON, Optimierungen OFF:

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

Debugging ON, Optimierungen ON:

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

Debugging OFF, Optimierungen ON:

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

Neue Überraschung. Durch den Wechsel von VS2008 (C # 3) zu VS2010 (C # 4) wird das UsingLambdaCombined schneller als das native Lambda.


Ok, ich habe einen Weg gefunden, die lambda kompilierte Performance um mehr als eine Größenordnung zu verbessern. Hier ist ein Tipp; Nach dem Ausführen des Profilers werden 92% der Zeit für Folgendes verwendet:

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

Hmmmm ... Warum wird bei jeder Iteration ein neuer Delegat erstellt? Ich bin mir nicht sicher, aber die Lösung folgt in einem separaten Post.

Akzeptierte Antwort

Könnte es sein, dass die inneren Lambdas nicht kompiliert werden?!? Hier ist ein Proof of 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);
    }

Und jetzt sind die Zeiten:

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

Woot! Nicht nur ist es schnell, es ist schneller als das native Lambda. ( Scratch Kopf ).


Natürlich ist der obige Code einfach zu schmerzhaft um zu schreiben. Lass uns eine einfache Magie machen:

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

Und einige Timings, VS2010, Optimierungen ON, Debugging OFF:

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

Nun könnte man argumentieren, dass ich den ganzen Ausdruck nicht dynamisch erzeuge; nur die Verkettungsaufrufe. Aber im obigen Beispiel erzeuge ich den ganzen Ausdruck. Und die Zeiten stimmen überein. Dies ist nur eine Abkürzung, um weniger Code zu schreiben.


Nach meinem Verständnis läuft die Methode .Compile () die Compilations nicht auf innere Lambdas und damit auf den konstanten Aufruf von CreateDelegate . Aber um das wirklich zu verstehen, hätte ich gerne einen .NET-Guru, der ein wenig über die internen Dinge spricht.

Und warum , oh warum ist das jetzt schneller als ein natives Lambda !?


Beliebte Antwort

Kürzlich habe ich eine fast identische Frage gestellt:

Leistung des kompilierten zu delegierenden Ausdrucks

Die Lösung für mich war, dass ich Compile auf dem Expression aufrufen sollte, aber dass ich CompileToMethod darauf aufrufen und den Expression zu einer static Methode in einer dynamischen Assembly kompilieren sollte.

Wie so:

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

Es ist jedoch nicht ideal. Ich bin nicht ganz sicher, auf welche Typen dies genau zutrifft, aber ich denke, dass Typen, die vom Delegaten als Parameter oder vom Delegierten zurückgegeben werden , public und nicht-generisch sein müssen. Es muss nicht-generisch sein, da generische Typen anscheinend auf System.__Canon welches ein interner Typ ist, der von .NET unter der Haube für generische Typen verwendet wird, und dies verstößt gegen die Regel " System.__Canon ein public Typ sein".

Für diese Typen können Sie den scheinbar langsameren Compile . Ich erkenne sie auf folgende Weise:

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

Aber wie ich schon sagte, das ist nicht ideal und ich würde gerne wissen, warum das Kompilieren einer Methode zu einer dynamischen Baugruppe manchmal eine Größenordnung schneller ist. Und ich sage manchmal, weil ich auch Fälle gesehen habe, in denen ein mit Compile kompilierter Expression genauso schnell ist wie eine normale Methode. Siehe dazu meine Frage.

Oder wenn jemand eine Möglichkeit kennt, die Einschränkung "keine nicht public Typen" mit der dynamischen Assemblierung zu umgehen, ist das ebenso willkommen.



Lizenziert unter: CC-BY-SA with attribution
Nicht verbunden mit Stack Overflow
Ist diese KB legal? Ja, lerne warum
Lizenziert unter: CC-BY-SA with attribution
Nicht verbunden mit Stack Overflow
Ist diese KB legal? Ja, lerne warum