Ich habe eine Frage zur Leistung von dynamic
in C #. Ich habe gelesen, dynamic
dass der Compiler wieder ausgeführt wird, aber was macht er?
Muss die gesamte Methode mit der dynamic
als Parameter verwendeten Variablen neu kompiliert werden oder nur mit den Zeilen mit dynamischem Verhalten / Kontext?
Ich habe festgestellt, dass die Verwendung von dynamic
Variablen eine einfache for-Schleife um 2 Größenordnungen verlangsamen kann.
Code, mit dem ich gespielt habe:
internal class Sum2
{
public int intSum;
}
internal class Sum
{
public dynamic DynSum;
public int intSum;
}
class Program
{
private const int ITERATIONS = 1000000;
static void Main(string[] args)
{
var stopwatch = new Stopwatch();
dynamic param = new Object();
DynamicSum(stopwatch);
SumInt(stopwatch);
SumInt(stopwatch, param);
Sum(stopwatch);
DynamicSum(stopwatch);
SumInt(stopwatch);
SumInt(stopwatch, param);
Sum(stopwatch);
Console.ReadKey();
}
private static void Sum(Stopwatch stopwatch)
{
var sum = 0;
stopwatch.Reset();
stopwatch.Start();
for (int i = 0; i < ITERATIONS; i++)
{
sum += i;
}
stopwatch.Stop();
Console.WriteLine(string.Format("Elapsed {0}", stopwatch.ElapsedMilliseconds));
}
private static void SumInt(Stopwatch stopwatch)
{
var sum = new Sum();
stopwatch.Reset();
stopwatch.Start();
for (int i = 0; i < ITERATIONS; i++)
{
sum.intSum += i;
}
stopwatch.Stop();
Console.WriteLine(string.Format("Class Sum int Elapsed {0}", stopwatch.ElapsedMilliseconds));
}
private static void SumInt(Stopwatch stopwatch, dynamic param)
{
var sum = new Sum2();
stopwatch.Reset();
stopwatch.Start();
for (int i = 0; i < ITERATIONS; i++)
{
sum.intSum += i;
}
stopwatch.Stop();
Console.WriteLine(string.Format("Class Sum int Elapsed {0} {1}", stopwatch.ElapsedMilliseconds, param.GetType()));
}
private static void DynamicSum(Stopwatch stopwatch)
{
var sum = new Sum();
stopwatch.Reset();
stopwatch.Start();
for (int i = 0; i < ITERATIONS; i++)
{
sum.DynSum += i;
}
stopwatch.Stop();
Console.WriteLine(String.Format("Dynamic Sum Elapsed {0}", stopwatch.ElapsedMilliseconds));
}
c#
performance
dynamic
Lukasz Madon
quelle
quelle
Antworten:
Das ist der Deal.
Für jeden Ausdruck in Ihrem Programm, der vom dynamischen Typ ist, gibt der Compiler Code aus, der ein einzelnes "dynamisches Aufrufstandortobjekt" generiert, das die Operation darstellt. So zum Beispiel, wenn Sie haben:
dann generiert der Compiler Code, der moralisch so ist. (Der eigentliche Code ist etwas komplexer; dies wird zu Präsentationszwecken vereinfacht.)
Sehen Sie, wie das bisher funktioniert? Wir generieren die Anrufstelle einmal , egal wie oft Sie M anrufen. Die Anrufstelle lebt für immer, nachdem Sie sie einmal generiert haben. Die Call-Site ist ein Objekt, das darstellt, dass "hier ein dynamischer Aufruf von Foo stattfinden wird".
OK, jetzt, wo Sie die Anrufseite haben, wie funktioniert der Aufruf?
Die Call-Site ist Teil der Dynamic Language Runtime. Das DLR sagt: "Hmm, jemand versucht, eine dynamische Methode für dieses Objekt aufzurufen. Weiß ich etwas darüber? Nein. Dann sollte ich es besser herausfinden."
Das DLR fragt dann das Objekt in d1 ab, um festzustellen, ob es etwas Besonderes ist. Möglicherweise handelt es sich um ein älteres COM-Objekt oder ein Iron Python-Objekt oder ein Iron Ruby-Objekt oder ein IE DOM-Objekt. Wenn es keines davon ist, muss es ein gewöhnliches C # -Objekt sein.
Dies ist der Punkt, an dem der Compiler erneut gestartet wird. Es ist kein Lexer oder Parser erforderlich, daher startet das DLR eine spezielle Version des C # -Compilers, die nur den Metadatenanalysator, den semantischen Analysator für Ausdrücke und einen Emitter enthält, der Ausdrucksbäume anstelle von IL ausgibt.
Der Metadatenanalysator verwendet Reflection, um den Typ des Objekts in d1 zu bestimmen, und übergibt diesen dann an den semantischen Analysator, um zu fragen, was passiert, wenn ein solches Objekt mit der Methode Foo aufgerufen wird. Der Überlastungsauflösungsanalysator ermittelt dies und erstellt dann einen Ausdrucksbaum - so als hätten Sie Foo in einem Ausdrucksbaum-Lambda aufgerufen -, der diesen Aufruf darstellt.
Der C # -Compiler übergibt diesen Ausdrucksbaum dann zusammen mit einer Cache-Richtlinie an das DLR zurück. Die Richtlinie lautet normalerweise "Wenn Sie ein Objekt dieses Typs zum zweiten Mal sehen, können Sie diesen Ausdrucksbaum wiederverwenden, anstatt mich erneut zurückzurufen". Das DLR ruft dann Compile für den Ausdrucksbaum auf, der den Ausdrucksbaum-zu-IL-Compiler aufruft und einen Block dynamisch generierter IL in einem Delegaten ausspuckt.
Das DLR speichert diesen Delegaten dann in einem Cache zwischen, der dem Aufrufstandortobjekt zugeordnet ist.
Dann ruft es den Delegaten auf und der Foo-Aufruf erfolgt.
Wenn Sie M zum zweiten Mal anrufen, haben wir bereits eine Anrufstelle. Das DLR fragt das Objekt erneut ab. Wenn das Objekt vom selben Typ ist wie beim letzten Mal, holt es den Delegaten aus dem Cache und ruft ihn auf. Wenn das Objekt von einem anderen Typ ist, fehlt der Cache und der gesamte Prozess beginnt von vorne. Wir führen eine semantische Analyse des Aufrufs durch und speichern das Ergebnis im Cache.
Dies geschieht für jeden Ausdruck , der dynamisch ist. Also zum Beispiel, wenn Sie haben:
Dann gibt es drei dynamische Anrufseiten. Eine für den dynamischen Aufruf von Foo, eine für die dynamische Addition und eine für die dynamische Konvertierung von dynamisch nach int. Jeder hat seine eigene Laufzeitanalyse und seinen eigenen Cache mit Analyseergebnissen.
Sinn ergeben?
quelle
Update: Vorkompilierte und faul kompilierte Benchmarks hinzugefügt
Update 2: Es stellt sich heraus, ich liege falsch. Eine vollständige und korrekte Antwort finden Sie in Eric Lipperts Beitrag. Ich lasse dies hier wegen der Benchmark-Zahlen
* Update 3: IL-Emitted- und Lazy IL-Emitted-Benchmarks hinzugefügt, basierend auf Mark Gravells Antwort auf diese Frage .
Meines Wissens führt die Verwendung desdynamic
Schlüsselworts zur Laufzeit an und für sich nicht zu einer zusätzlichen Kompilierung (obwohl ich mir vorstellen kann, dass dies unter bestimmten Umständen möglich ist, je nachdem, welche Art von Objekten Ihre dynamischen Variablen unterstützen).In Bezug auf die Leistung führt
dynamic
dies von Natur aus zu einem gewissen Overhead, jedoch bei weitem nicht so viel, wie Sie vielleicht denken. Zum Beispiel habe ich gerade einen Benchmark erstellt, der so aussieht:Wie Sie dem Code entnehmen können, versuche ich, eine einfache No-Op-Methode auf sieben verschiedene Arten aufzurufen:
dynamic
Action
, das zur Laufzeit vorkompiliert wurde (wodurch die Kompilierungszeit von den Ergebnissen ausgeschlossen wird).Action
, die beim ersten Mal kompiliert wird, wenn sie benötigt wird, Verwenden einer nicht threadsicheren Lazy-Variablen (einschließlich Kompilierungszeit)Jeder wird in einer einfachen Schleife 1 Million Mal aufgerufen. Hier sind die Timing-Ergebnisse:
Während die Verwendung des
dynamic
Schlüsselworts eine Größenordnung länger dauert als der direkte Aufruf der Methode, gelingt es ihm dennoch, den Vorgang millionenfach in etwa 50 Millisekunden abzuschließen, was ihn weitaus schneller als die Reflexion macht. Wenn die von uns aufgerufene Methode versuchen würde, etwas Intensives zu tun, z. B. einige Zeichenfolgen miteinander zu kombinieren oder eine Sammlung nach einem Wert zu durchsuchen, würden diese Operationen wahrscheinlich den Unterschied zwischen einem direkten Aufruf und einemdynamic
Aufruf bei weitem überwiegen .Die Leistung ist nur einer von vielen guten Gründen, sie nicht
dynamic
unnötig zu verwenden. Wenn Sie jedoch mit echtendynamic
Daten arbeiten, kann sie Vorteile bieten, die die Nachteile bei weitem überwiegen.Update 4
Basierend auf Johnbots Kommentar habe ich den Reflexionsbereich in vier separate Tests unterteilt:
... und hier sind die Benchmark-Ergebnisse:
Wenn Sie also eine bestimmte Methode vorgeben können, die Sie häufig aufrufen müssen, ist das Aufrufen eines zwischengespeicherten Delegaten, der auf diese Methode verweist, ungefähr so schnell wie das Aufrufen der Methode selbst. Wenn Sie jedoch festlegen müssen, welche Methode aufgerufen werden soll, während Sie sie aufrufen, ist das Erstellen eines Delegaten dafür sehr teuer.
quelle
dynamic
natürlich verlieren:public class ONE<T>{public object i { get; set; }public ONE(){i = typeof(T).ToString();}public object make(int ix){ if (ix == 0) return i;ONE<ONE<T>> x = new ONE<ONE<T>>();/*dynamic x = new ONE<ONE<T>>();*/return x.make(ix - 1);}}ONE<END> x = new ONE<END>();string lucky;Stopwatch sw = new Stopwatch();sw.Start();lucky = (string)x.make(500);sw.Stop();Trace.WriteLine(sw.ElapsedMilliseconds);Trace.WriteLine(lucky);
var methodDelegate = (Action)method.CreateDelegate(typeof(Action), foo);