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);
Verwenden wir jetzt Ausdrücke. Der folgende Code entspricht in etwa:
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 on-the-fly erstellen, daher hier 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, ändere 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 ausgeschaltetem Debugging:
Using lambda compiled: 21718750
Using lambda: 937500
Using lambda combined: 1093750
Überraschung . Der kompilierte Ausdruck ist ungefähr 17x langsamer als die anderen Alternativen. Nun kommen hier die Fragen:
- Vergleiche ich nicht äquivalente Ausdrücke?
- Gibt es einen Mechanismus, mit dem .NET den kompilierten Ausdruck "optimiert"?
- Wie drücke ich denselben Kettenaufruf
l.Where(i => i % 2 == 0).Where(i => i > 5);
programmgesteuert aus?
Noch ein paar Statistiken. Visual Studio 2010, Debugging EIN, Optimierungen AUS:
Using lambda: 1093974
Using lambda compiled: 15315636
Using lambda combined: 781410
Debugging EIN, Optimierungen EIN:
Using lambda: 781305
Using lambda compiled: 15469839
Using lambda combined: 468783
Debugging AUS, Optimierungen EIN:
Using lambda: 625020
Using lambda compiled: 14687970
Using lambda combined: 468765
Neue Überraschung. Der Wechsel von VS2008 (C # 3) zu VS2010 (C # 4) macht das UsingLambdaCombined
Lambda schneller als das native.
Ok, ich habe einen Weg gefunden, die kompilierte Lambda-Leistung 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 aufgewendet:
System.Reflection.Emit.DynamicMethod.CreateDelegate(class System.Type, object)
Hmmmm ... Warum wird in jeder Iteration ein neuer Delegat erstellt? Ich bin nicht sicher, aber die Lösung folgt in einem separaten Beitrag.
quelle
Stopwatch
Ziehen Sie auch in Betracht, für Timings anstatt zu verwendenDateTime.Now
.Antworten:
Könnte es sein, dass die inneren Lambdas nicht zusammengestellt werden?!? Hier ist ein Proof of Concept:
Und jetzt sind die Zeiten:
Woot! Es ist nicht nur schnell, es ist auch schneller als das einheimische Lambda. ( Kratzkopf ).
Natürlich ist der obige Code einfach zu schmerzhaft zum Schreiben. Lassen Sie uns eine einfache Magie machen:
Und einige Timings, VS2010, Optimierungen EIN, Debuggen AUS:
Jetzt könnte man argumentieren, dass ich nicht den gesamten Ausdruck dynamisch generiere; nur die Verkettungsaufrufe. Aber im obigen Beispiel generiere ich den gesamten Ausdruck. Und die Zeiten stimmen überein. Dies ist nur eine Verknüpfung, um weniger Code zu schreiben.
Nach meinem Verständnis geht es darum, dass die .Compile () -Methode die Kompilierungen nicht an innere Lambdas weitergibt und somit den ständigen Aufruf von
CreateDelegate
. Aber um dies wirklich zu verstehen, würde ich gerne einen .NET-Guru dazu bringen, ein wenig über die internen Dinge zu kommentieren.Und warum , oh warum ist das jetzt schneller als ein einheimisches Lambda?
quelle
Kürzlich habe ich eine fast identische Frage gestellt:
Leistung des zum Delegieren kompilierten Ausdrucks
Die Lösung für mich war , dass ich nicht nennen sollte
Compile
auf dasExpression
, aber das nenne ich sollteCompileToMethod
darauf und kompilieren dieExpression
zu einemstatic
Verfahren in einer dynamischen Montage.Wie so:
Es ist jedoch nicht ideal. Ich bin nicht ganz sicher, für welche Typen dies genau gilt, aber ich denke, dass Typen, die vom Delegaten als Parameter verwendet oder vom Delegaten zurückgegeben werden ,
public
nicht generisch sein müssen. Es muss nicht generisch sein, da generische Typen anscheinend aufSystem.__Canon
einen internen Typ zugreifen, der von .NET unter der Haube für generische Typen verwendet wird, und dies gegen diepublic
Regel "muss eine Typregel sein" verstößt .Für diese Typen können Sie den scheinbar langsameren verwenden
Compile
. Ich erkenne sie folgendermaßen:Aber wie gesagt, das ist nicht ideal und ich würde immer noch gerne wissen, warum das Kompilieren einer Methode zu einer dynamischen Assembly manchmal eine Größenordnung schneller ist. Und ich sage manchmal, weil ich auch Fälle gesehen habe, in denen eine
Expression
KompilierungCompile
genauso schnell ist wie eine normale Methode. Siehe meine Frage dazu.Oder wenn jemand eine Möglichkeit kennt, die
public
Einschränkung "Keine Nicht- Typen" mit der dynamischen Assembly zu umgehen , ist dies ebenfalls willkommen.quelle
Ihre Ausdrücke sind nicht gleichwertig und Sie erhalten daher verzerrte Ergebnisse. Ich habe einen Prüfstand geschrieben, um dies zu testen. Die Tests umfassen den regulären Lambda-Aufruf, den äquivalenten kompilierten Ausdruck, einen handgemachten äquivalenten kompilierten Ausdruck sowie komponierte Versionen. Dies sollten genauere Zahlen sein. Interessanterweise sehe ich keine großen Unterschiede zwischen der einfachen und der komponierten Version. Und die kompilierten Ausdrücke sind natürlich langsamer, aber nur sehr wenig. Sie benötigen eine ausreichend große Eingabe- und Iterationszahl, um einige gute Zahlen zu erhalten. Es macht einen Unterschied.
Was Ihre zweite Frage betrifft, weiß ich nicht, wie Sie mehr Leistung daraus ziehen können, daher kann ich Ihnen dort nicht helfen. Es sieht so gut aus, wie es nur geht.
Meine Antwort auf Ihre dritte Frage finden Sie in der
HandMadeLambdaExpression()
Methode. Aufgrund der Erweiterungsmethoden nicht der am einfachsten zu erstellende Ausdruck, aber machbar.Und die Ergebnisse auf meiner Maschine:
quelle
Die kompilierte Lambda-Leistung über Delegaten ist möglicherweise langsamer, da kompilierter Code zur Laufzeit möglicherweise nicht optimiert wird. Der manuell geschriebene und über den C # -Compiler kompilierte Code wird jedoch optimiert.
Zweitens bedeuten mehrere Lambda-Ausdrücke mehrere anonyme Methoden, und das Aufrufen jeder dieser Methoden erfordert wenig zusätzliche Zeit für die Bewertung einer geraden Methode. Zum Beispiel anrufen
und
sind unterschiedlich, und mit dem zweiten ist etwas mehr Overhead erforderlich, als aus Sicht des Compilers, es sind tatsächlich zwei verschiedene Aufrufe. Rufen Sie zuerst x selbst auf und dann innerhalb der Anweisung von x.
Ihr kombinierter Lambda hat also sicherlich eine geringe langsame Leistung gegenüber einem einzelnen Lambda-Ausdruck.
Dies ist unabhängig von der Ausführung im Inneren, da Sie immer noch die korrekte Logik auswerten, aber zusätzliche Schritte für den Compiler hinzufügen.
Selbst nachdem der Ausdrucksbaum kompiliert wurde, wird er nicht optimiert, und seine kleine komplexe Struktur bleibt erhalten. Wenn er ausgewertet und aufgerufen wird, kann es zu einer zusätzlichen Validierung, Nullprüfung usw. kommen, was die Leistung kompilierter Lambda-Ausdrücke verlangsamen kann.
quelle
UsingLambdaCombined
kombiniert der Test mehrere Lambda-Funktionen und seine Leistung ist sehr naheUsingLambda
. In Bezug auf die Optimierungen war ich davon überzeugt, dass sie von der JIT-Engine verarbeitet werden und daher zur Laufzeit generierter Code (nach der Kompilierung) auch Ziel von JIT-Optimierungen sein würde.