Prestazioni delle espressioni Lambda compilate in C #


91

Considera la seguente semplice manipolazione su una raccolta:

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

Ora usiamo le espressioni. Il codice seguente è più o meno equivalente:

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

Ma voglio costruire l'espressione al volo, quindi ecco un nuovo 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);
}

Ovviamente non è esattamente come sopra, quindi per essere onesti, modifico leggermente il primo:

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

Ora arrivano i risultati per MAX = 100000, VS2008, debug ON:

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

E con debug disattivato:

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

Sorpresa . L'espressione compilata è circa 17 volte più lenta delle altre alternative. Ora arrivano le domande:

  1. Sto confrontando espressioni non equivalenti?
  2. Esiste un meccanismo per fare in modo che .NET "ottimizzi" l'espressione compilata?
  3. Come si esprime la stessa chiamata a catena in modo l.Where(i => i % 2 == 0).Where(i => i > 5);programmatico?

Altre statistiche. Visual Studio 2010, debug attivato, ottimizzazioni disattivate:

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

Debug ON, ottimizzazioni ON:

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

Debug disattivato, ottimizzazioni attivate:

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

Nuova sorpresa. Il passaggio da VS2008 (C # 3) a VS2010 (C # 4), rende il UsingLambdaCombinedpiù veloce del lambda nativo.


Ok, ho trovato un modo per migliorare le prestazioni compilate lambda di più di un ordine di grandezza. Ecco un consiglio; dopo aver eseguito il profiler, il 92% del tempo viene dedicato a:

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

Hmmmm ... Perché crea un nuovo delegato in ogni iterazione? Non ne sono sicuro, ma la soluzione segue in un post separato.


3
Questi tempi vengono eseguiti in Visual Studio? In tal caso, ripetere i tempi utilizzando una build in modalità di rilascio ed eseguire senza eseguire il debug (ad esempio Ctrl + F5 in Visual Studio o dalla riga di comando). Inoltre, considera l'utilizzo Stopwatchper i tempi anziché DateTime.Now.
Jim Mischel

12
Non so perché sia ​​più lento, ma la tua tecnica di benchmark non è molto buona. Prima di tutto, DateTime.Now è preciso solo a 1/64 di secondo, quindi l'errore di arrotondamento delle misurazioni è grande. Usa invece il cronometro; è accurato fino a pochi nanosecondi. Secondo, stai misurando sia il tempo necessario per jit il codice (la prima chiamata) che ogni chiamata successiva; che può buttare fuori le medie. (Anche se in questo caso un MAX di centomila è probabilmente sufficiente per calcolare la media del carico di jit, tuttavia, è una cattiva pratica includerlo nella media.)
Eric Lippert

7
@Eric, l'errore di arrotondamento può essere presente solo se in ciascuna operazione viene utilizzato DateTime.Now.Ticks, prima dell'inizio e dopo la fine, i conteggi dei millisecondi sono sufficientemente alti da mostrare la differenza di prestazioni.
Akash Kava

1
se si utilizza il cronometro, consiglio di seguire questo articolo per garantire risultati accurati: codeproject.com/KB/testing/stopwatch-measure-precise.aspx
Zach Green

1
@Eric, anche se sono d'accordo che non è la tecnica di misurazione più precisa disponibile, stiamo parlando di un ordine di grandezza di differenza. MAX è sufficientemente alto da ridurre deviazioni significative.
Hugo Sereno Ferreira

Risposte:


43

Potrebbe essere che i lambda interni non vengano compilati?!? Ecco una prova di concetto:

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

E ora i tempi sono:

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

Woot! Non solo è veloce, è più veloce del lambda nativo. ( Gratta testa ).


Ovviamente il codice sopra è semplicemente troppo doloroso da scrivere. Facciamo qualche semplice magia:

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

E alcuni tempi, VS2010, ottimizzazioni ON, debug OFF:

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

Ora potresti sostenere che non sto generando l'intera espressione dinamicamente; solo le invocazioni concatenate. Ma nell'esempio sopra ho generato l'intera espressione. E i tempi corrispondono. Questa è solo una scorciatoia per scrivere meno codice.


Dalla mia comprensione, quello che sta succedendo è che il metodo .Compile () non propaga le compilazioni ai lambda interni, e quindi la costante invocazione di CreateDelegate. Ma per capirlo veramente, mi piacerebbe che un guru di .NET commentasse un po 'le cose interne in corso.

E perché , oh perché ora è più veloce di un lambda nativo !?


1
Sto pensando di accettare la mia risposta, visto che è quella con i voti più positivi. Devo aspettare ancora un po '?
Hugo Sereno Ferreira

Per quanto riguarda ciò che accade quando ottieni codice più velocemente del lambda nativo, potresti dare un'occhiata a questa pagina sui microbenchmark (che non ha nulla di veramente specifico per Java, nonostante il nome): code.google.com/p/caliper/wiki / JavaMicrobenchmarks
Blaisorblade

Per quanto riguarda il motivo per cui il lambda compilato dinamicamente è più veloce, sospetto che "l'utilizzo di lambda", eseguito per primo, sia penalizzato dal dover eseguire il JIT di un po 'di codice.
Oskar Berggren

Non so cosa stia succedendo, una volta quando ho testato l'espressione compilata e creatoelegate per l'impostazione e l'ottenimento da campi e proprietà, createdelegate era molto più veloce per le proprietà, ma compilato era leggermente più veloce per i campi
nawfal

10

Di recente ho posto una domanda quasi identica:

Prestazioni dell'espressione da compilato a delegato

La soluzione per me era che non si deve dire Compilein Expression, ma che avrei dovuto chiamare CompileToMethodsu di esso e compilare il Expressionad un staticmetodo in un dinamica di montaggio.

Così:

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

Tuttavia non è l'ideale. Non sono abbastanza sicuro di quali tipi si applichi esattamente, ma penso che i tipi che vengono presi come parametri dal delegato o restituiti dal delegato debbano essere publice non generici. Deve essere non generico perché i tipi generici apparentemente accedono System.__Canonche è un tipo interno usato da .NET sotto il cofano per i tipi generici e questo viola la publicregola "deve essere una regola di tipo).

Per quei tipi, puoi usare l'apparentemente più lento Compile . Li rilevo nel modo seguente:

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

Ma come ho detto, questo non è l'ideale e vorrei comunque sapere perché lo è la compilazione di un metodo in un assembly dinamico volte un ordine di grandezza più veloce. E dico a volte perché ho anche visto casi in cui un Expressioncompilato con Compileè veloce quanto un metodo normale. Vedi la mia domanda per questo.

O se qualcuno conosce un modo per aggirare il publicvincolo "no non- types" con l'assieme dinamico, anche questo è il benvenuto.


4

Le tue espressioni non sono equivalenti e quindi ottieni risultati distorti. Ho scritto un banco di prova per testarlo. I test includono la chiamata lambda regolare, l'espressione compilata equivalente, un'espressione compilata equivalente fatta a mano, nonché versioni composte. Questi dovrebbero essere numeri più precisi. È interessante notare che non vedo molte variazioni tra la versione semplice e quella composta. E le espressioni compilate sono naturalmente più lente ma solo di molto poco. È necessario un input e un numero di iterazioni sufficientemente grandi per ottenere dei buoni numeri. Fa la differenza.

Per quanto riguarda la tua seconda domanda, non so come potresti ottenere più prestazioni da questo, quindi non posso aiutarti. Sembra buono come sta per arrivare.

Troverai la mia risposta alla tua terza domanda nel HandMadeLambdaExpression()metodo. Non è l'espressione più semplice da costruire a causa dei metodi di estensione, ma fattibile.

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

E i risultati sulla mia macchina:

Lambda:
  Risposta: 340971948 123230 (ms)
  Media: 340,971948 0,12323 (ms)
Espressione lambda:
  Risposta: 357077202 129051 (ms)
  Media: 357.077202 0.129051 (ms)
Espressione Lambda fatta a mano:
  Risposta: 345029281 124696 (ms)
  Media: 345.029281 0.124696 (ms)

Composto:
  Risposta: 340409238 123027 (ms)
  Media: 340.409238 0.123027 (ms)
Espressione composta:
  Risposta: 350800599 126782 (ms)
  Media: 350.800599 0.126782 (ms)
Espressione composta a mano:
  Risposta: 352811359 127509 (ms)
  Media: 352,811359 0,127509 (ms)

3

Le prestazioni lambda compilate sui delegati potrebbero essere più lente perché il codice compilato in fase di esecuzione potrebbe non essere ottimizzato, tuttavia il codice scritto manualmente e quello compilato tramite il compilatore C # è ottimizzato.

In secondo luogo, più espressioni lambda significano più metodi anonimi e la chiamata a ciascuno di essi richiede poco tempo in più rispetto alla valutazione di un metodo semplice. Ad esempio, chiamando

Console.WriteLine(x);

e

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

sono diversi, e con il secondo è richiesto un po 'più di overhead dal punto di vista del compilatore, in realtà sono due chiamate diverse. Prima chiamando x stesso e poi all'interno dell'istruzione di tale x.

Quindi il tuo Lambda combinato avrà sicuramente prestazioni poco lente rispetto alla singola espressione lambda.

E questo è indipendente da ciò che viene eseguito all'interno, perché stai ancora valutando la logica corretta, ma stai aggiungendo ulteriori passaggi per l'esecuzione del compilatore.

Anche dopo che l'albero delle espressioni è stato compilato, non avrà ottimizzazione e manterrà la sua struttura poco complessa, valutandolo e chiamandolo potrebbe avere una convalida extra, un controllo nullo, ecc.Che potrebbe rallentare le prestazioni delle espressioni lambda compilate.


2
Se guardi da vicino, il UsingLambdaCombinedtest combina più funzioni lambda e le sue prestazioni sono molto vicine a UsingLambda. Per quanto riguarda le ottimizzazioni, ero convinto che fossero gestite dal motore JIT, e quindi il codice generato in runtime (dopo la compilazione), sarebbe stato anche bersaglio di eventuali ottimizzazioni JIT.
Hugo Sereno Ferreira

1
L'ottimizzazione JIT e l'ottimizzazione del tempo di compilazione sono due cose diverse che puoi disattivare l'ottimizzazione del tempo di compilazione nelle impostazioni del progetto. In secondo luogo, la compilazione di espressioni probabilmente emetterà MSIL dinamico che sarà di nuovo un po 'più lento poiché la sua logica e la sequenza di operazioni conterranno controlli nulli e validità secondo le esigenze. Puoi guardare nel riflettore per sapere come è compilato.
Akash Kava

2
Sebbene il tuo ragionamento sia corretto, devo essere in disaccordo con te su questo particolare problema (cioè, la differenza dell'ordine di grandezza non è dovuta alla compilazione statica). Primo, perché se disabiliti effettivamente le ottimizzazioni in fase di compilazione, la differenza è ancora considerevole. In secondo luogo, perché ho già trovato un modo per ottimizzare la generazione dinamica in modo che sia solo leggermente più lenta. Fammi provare a capire il "perché" e posterò i risultati.
Hugo Sereno Ferreira
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.