Leistung des kompilierten zu delegierenden Ausdrucks

c# dynamically-generated expression-trees performance

Frage

Ich erzeuge einen Ausdruckbaum, der Eigenschaften von einem Func<TSource, TDestination, TDestination> auf ein Func<TSource, TDestination, TDestination> das dann zu einem Func<TSource, TDestination, TDestination> und ausgeführt wird.

Dies ist die Debug-Ansicht der resultierenden LambdaExpression :

.Lambda #Lambda1<System.Func`3[MemberMapper.Benchmarks.Program+ComplexSourceType,MemberMapper.Benchmarks.Program+ComplexDestinationType,MemberMapper.Benchmarks.Program+ComplexDestinationType]>(
    MemberMapper.Benchmarks.Program+ComplexSourceType $right,
    MemberMapper.Benchmarks.Program+ComplexDestinationType $left) {
    .Block(
        MemberMapper.Benchmarks.Program+NestedSourceType $Complex$955332131,
        MemberMapper.Benchmarks.Program+NestedDestinationType $Complex$2105709326) {
        $left.ID = $right.ID;
        $Complex$955332131 = $right.Complex;
        $Complex$2105709326 = .New MemberMapper.Benchmarks.Program+NestedDestinationType();
        $Complex$2105709326.ID = $Complex$955332131.ID;
        $Complex$2105709326.Name = $Complex$955332131.Name;
        $left.Complex = $Complex$2105709326;
        $left
    }
}

Aufgeräumt wäre es:

.Lambda #Lambda1<System.Func`3[MemberMapper.Benchmarks.Program+ComplexSourceType,MemberMapper.Benchmarks.Program+ComplexDestinationType,MemberMapper.Benchmarks.Program+ComplexDestinationType]>(
    MemberMapper.Benchmarks.Program+ComplexSourceType $right,
    MemberMapper.Benchmarks.Program+ComplexDestinationType $left) {
    .Block(
        MemberMapper.Benchmarks.Program+NestedSourceType $Complex$955332131,
        MemberMapper.Benchmarks.Program+NestedDestinationType $Complex$2105709326) {
        $left.ID = $right.ID;
        $Complex$955332131 = $right.Complex;
        $Complex$2105709326 = .New MemberMapper.Benchmarks.Program+NestedDestinationType();
        $Complex$2105709326.ID = $Complex$955332131.ID;
        $Complex$2105709326.Name = $Complex$955332131.Name;
        $left.Complex = $Complex$2105709326;
        $left
    }
}

Das ist der Code, der die Eigenschaften dieser Typen abbildet:

.Lambda #Lambda1<System.Func`3[MemberMapper.Benchmarks.Program+ComplexSourceType,MemberMapper.Benchmarks.Program+ComplexDestinationType,MemberMapper.Benchmarks.Program+ComplexDestinationType]>(
    MemberMapper.Benchmarks.Program+ComplexSourceType $right,
    MemberMapper.Benchmarks.Program+ComplexDestinationType $left) {
    .Block(
        MemberMapper.Benchmarks.Program+NestedSourceType $Complex$955332131,
        MemberMapper.Benchmarks.Program+NestedDestinationType $Complex$2105709326) {
        $left.ID = $right.ID;
        $Complex$955332131 = $right.Complex;
        $Complex$2105709326 = .New MemberMapper.Benchmarks.Program+NestedDestinationType();
        $Complex$2105709326.ID = $Complex$955332131.ID;
        $Complex$2105709326.Name = $Complex$955332131.Name;
        $left.Complex = $Complex$2105709326;
        $left
    }
}

Der manuelle Code hierfür lautet:

.Lambda #Lambda1<System.Func`3[MemberMapper.Benchmarks.Program+ComplexSourceType,MemberMapper.Benchmarks.Program+ComplexDestinationType,MemberMapper.Benchmarks.Program+ComplexDestinationType]>(
    MemberMapper.Benchmarks.Program+ComplexSourceType $right,
    MemberMapper.Benchmarks.Program+ComplexDestinationType $left) {
    .Block(
        MemberMapper.Benchmarks.Program+NestedSourceType $Complex$955332131,
        MemberMapper.Benchmarks.Program+NestedDestinationType $Complex$2105709326) {
        $left.ID = $right.ID;
        $Complex$955332131 = $right.Complex;
        $Complex$2105709326 = .New MemberMapper.Benchmarks.Program+NestedDestinationType();
        $Complex$2105709326.ID = $Complex$955332131.ID;
        $Complex$2105709326.Name = $Complex$955332131.Name;
        $left.Complex = $Complex$2105709326;
        $left
    }
}

Das Problem ist, dass wenn ich den LambdaExpression kompiliere und den resultierenden delegate benchmarkiere, ist es etwa 10x langsamer als die manuelle Version. Ich habe keine Ahnung, warum das so ist. Und die ganze Vorstellung davon ist maximale Leistung ohne die Langeweile des manuellen Zuordnens.

Wenn ich Code von Bart de Smet aus seinem Blogbeitrag zu diesem Thema nehme und die manuelle Version der Berechnung von Primzahlen mit dem kompilierten Ausdrucksbaum vergleicht, sind sie in ihrer Leistung völlig identisch.

Was kann diesen großen Unterschied verursachen, wenn die Debug-Ansicht der LambdaExpression wie LambdaExpression aussieht?

BEARBEITEN

Wie gewünscht, habe ich den Benchmark hinzugefügt, den ich verwendet habe:

.Lambda #Lambda1<System.Func`3[MemberMapper.Benchmarks.Program+ComplexSourceType,MemberMapper.Benchmarks.Program+ComplexDestinationType,MemberMapper.Benchmarks.Program+ComplexDestinationType]>(
    MemberMapper.Benchmarks.Program+ComplexSourceType $right,
    MemberMapper.Benchmarks.Program+ComplexDestinationType $left) {
    .Block(
        MemberMapper.Benchmarks.Program+NestedSourceType $Complex$955332131,
        MemberMapper.Benchmarks.Program+NestedDestinationType $Complex$2105709326) {
        $left.ID = $right.ID;
        $Complex$955332131 = $right.Complex;
        $Complex$2105709326 = .New MemberMapper.Benchmarks.Program+NestedDestinationType();
        $Complex$2105709326.ID = $Complex$955332131.ID;
        $Complex$2105709326.Name = $Complex$955332131.Name;
        $left.Complex = $Complex$2105709326;
        $left
    }
}

Der zweite ist verständlicherweise langsamer als manuell, da es eine Wörterbuchsuche und einige Objektinstanziierungen erfordert, aber der dritte sollte genauso schnell sein wie der rohe Delegat, der aufgerufen wird, und der Cast von Delegate zu Func geschieht außerhalb des Schleife.

Ich habe auch versucht, den manuellen Code in einer Funktion zu verpacken, aber ich erinnere mich, dass es keinen merklichen Unterschied gemacht hat. In jedem Fall sollte ein Funktionsaufruf keine Größenordnung von Overhead hinzufügen.

Ich mache auch den Benchmark zweimal, um sicherzustellen, dass das JIT nicht interferiert.

BEARBEITEN

Sie können den Code für dieses Projekt hier abrufen:

https://github.com/JulianR/MemberMapper/

Ich habe die Sons-of-Strike-Debugger-Erweiterung verwendet, wie in diesem Blogbeitrag von Bart de Smet beschrieben, um die generierte IL der dynamischen Methode auszugeben:

.Lambda #Lambda1<System.Func`3[MemberMapper.Benchmarks.Program+ComplexSourceType,MemberMapper.Benchmarks.Program+ComplexDestinationType,MemberMapper.Benchmarks.Program+ComplexDestinationType]>(
    MemberMapper.Benchmarks.Program+ComplexSourceType $right,
    MemberMapper.Benchmarks.Program+ComplexDestinationType $left) {
    .Block(
        MemberMapper.Benchmarks.Program+NestedSourceType $Complex$955332131,
        MemberMapper.Benchmarks.Program+NestedDestinationType $Complex$2105709326) {
        $left.ID = $right.ID;
        $Complex$955332131 = $right.Complex;
        $Complex$2105709326 = .New MemberMapper.Benchmarks.Program+NestedDestinationType();
        $Complex$2105709326.ID = $Complex$955332131.ID;
        $Complex$2105709326.Name = $Complex$955332131.Name;
        $left.Complex = $Complex$2105709326;
        $left
    }
}

Ich bin kein Experte in IL, aber das scheint ziemlich geradlinig und genau das, was Sie erwarten würden, nein? Warum ist es so langsam? Keine seltsamen Boxoperationen, keine versteckten Instanziierungen, nichts. Es ist nicht genau das selbe wie der obige Ausdruckbaum, da es auch right.Complex einen null Check right.Complex .

Dies ist der Code für die manuelle Version (erhalten über Reflector):

.Lambda #Lambda1<System.Func`3[MemberMapper.Benchmarks.Program+ComplexSourceType,MemberMapper.Benchmarks.Program+ComplexDestinationType,MemberMapper.Benchmarks.Program+ComplexDestinationType]>(
    MemberMapper.Benchmarks.Program+ComplexSourceType $right,
    MemberMapper.Benchmarks.Program+ComplexDestinationType $left) {
    .Block(
        MemberMapper.Benchmarks.Program+NestedSourceType $Complex$955332131,
        MemberMapper.Benchmarks.Program+NestedDestinationType $Complex$2105709326) {
        $left.ID = $right.ID;
        $Complex$955332131 = $right.Complex;
        $Complex$2105709326 = .New MemberMapper.Benchmarks.Program+NestedDestinationType();
        $Complex$2105709326.ID = $Complex$955332131.ID;
        $Complex$2105709326.Name = $Complex$955332131.Name;
        $left.Complex = $Complex$2105709326;
        $left
    }
}

Sieht mit mir identisch aus ..

BEARBEITEN

Ich folgte dem Link in Michael B's Antwort zu diesem Thema. Ich habe versucht, den Trick in der akzeptierten Antwort zu implementieren und es hat funktioniert! Wenn Sie eine Zusammenfassung des Tricks wünschen: Er erstellt eine dynamische Assembly und kompiliert den Ausdrucksbaum in eine statische Methode in dieser Assembly und aus irgendeinem Grund ist das 10x schneller. Ein Nachteil davon ist, dass meine Benchmark-Klassen intern waren (eigentlich öffentliche Klassen, die in einem internen verschachtelt waren) und es gab eine Ausnahme, als ich versuchte, auf sie zuzugreifen, weil sie nicht zugänglich waren. Es scheint keine Problemumgehung zu geben, aber ich kann einfach feststellen, ob die referenzierten Typen intern sind oder nicht, und entscheiden, welche Art der Kompilierung verwendet werden soll.

Was mich immer noch stört ist, warum diese Primzahlmethode in der Leistung mit der kompilierten Ausdrucksbaumstruktur identisch ist .

Und noch einmal, ich begrüße jeden, der den Code in diesem GitHub-Repository ausführt, um meine Messungen zu bestätigen und sicherzustellen, dass ich nicht verrückt bin :)

Akzeptierte Antwort

Das ist ziemlich seltsam für solch ein riesiges Gespräch. Es gibt ein paar Dinge zu beachten. Zunächst werden dem VS-kompilierten Code verschiedene Eigenschaften zugewiesen, die den Jitter möglicherweise unterschiedlich optimieren.

Beinhalten Sie die erste Ausführung für den kompilierten Delegaten in diesen Ergebnissen? Sie sollten nicht, sollten Sie die erste Ausführung eines der beiden Code-Pfad ignorieren. Sie sollten den normalen Code auch in einen Delegaten umwandeln, da der Aufruf von Delegierten etwas langsamer ist als das Aufrufen einer Instanzmethode, die langsamer ist als das Aufrufen einer statischen Methode.

Wie für andere Änderungen gibt es etwas, das für die Tatsache verantwortlich ist, dass der kompilierte Delegat ein Closure-Objekt hat, das hier nicht verwendet wird, aber das bedeutet, dass es sich um einen gezielten Delegaten handelt, der möglicherweise etwas langsamer arbeitet. Sie werden feststellen, dass der kompilierte Delegat über ein Zielobjekt verfügt und alle Argumente um eins nach unten verschoben sind.

Auch Methoden, die von lcg generiert werden, werden als statisch betrachtet, da sie aufgrund von Registerwechsel-Geschäften tendenziell langsamer sind, wenn sie auf Delegaten als Instanzmethoden kompiliert werden. (Duffy sagte, dass der "this" -Zeiger ein reserviertes Register in CLR hat und wenn Sie einen Delegaten für eine statische Variable haben, muss er in ein anderes Register verschoben werden, was einen leichten Overhead verursacht). Schließlich scheint der zur Laufzeit generierte Code etwas langsamer zu laufen als der von VS generierte Code. Code, der zur Laufzeit erzeugt wird, scheint zusätzliches Sandboxing zu haben und wird von einer anderen Assembly gestartet (versuchen Sie etwas wie ldftn opcode oder calli opcode, wenn Sie mir nicht glauben, diese reflection.emited Delegierten kompilieren, lassen Sie sie aber nicht ausführen ), die einen minimalen Overhead verursacht.

Du rennst auch im Freigabemodus richtig? Es gab ein ähnliches Thema, in dem wir dieses Problem hier untersuchten: Warum wird Func <> aus Expression <Func <>> langsamer als Func <> direkt deklariert?

Bearbeiten: Siehe auch meine Antwort hier: DynamicMethod ist viel langsamer als kompilierte IL-Funktion

Das Wichtigste dabei ist, dass Sie der Assembly, in der Sie den generierten Laufzeitcode erstellen und aufrufen möchten, den folgenden Code hinzufügen sollten.

[assembly: AllowPartiallyTrustedCallers]
[assembly: SecurityTransparent]
[assembly: SecurityRules(SecurityRuleSet.Level2,SkipVerificationInFullTrust=true)]

Und immer einen integrierten Delegattyp oder einen aus einer Assembly mit diesen Flags verwenden.

Der Grund dafür ist, dass anonymer dynamischer Code in einer Assembly gehostet wird, die immer als teilweise vertrauenswürdig gekennzeichnet ist. Wenn Sie teilweise vertrauenswürdige Anrufer zulassen, können Sie einen Teil des Handshakes überspringen. Die Transparenz bedeutet, dass Ihr Code die Sicherheitsstufe nicht erhöht (z. B. langsames Verhalten). Und schließlich besteht der wahre Trick darin, einen Delegattyp aufzurufen, der in einer Assembly gehostet wird, die als Überprüfungsüberprüfung gekennzeichnet ist. Func<int,int>#Invoke ist voll vertrauenswürdig, daher ist keine Überprüfung erforderlich. Dadurch erhalten Sie die Leistung des vom VS-Compiler generierten Codes. Wenn Sie diese Attribute nicht verwenden, betrachten Sie einen Aufwand in .NET 4. Sie könnten denken, dass SecurityRuleSet.Level1 eine gute Möglichkeit ist, diesen Overhead zu vermeiden, aber das Wechseln von Sicherheitsmodellen ist ebenfalls teuer.

Kurz gesagt, fügen Sie diese Attribute hinzu, und dann wird Ihr Micro-Loop-Leistungstest ungefähr gleich laufen.


Beliebte Antwort

Es hört sich so an, als würden Sie in den Overhead der Aufrufe geraten. Unabhängig davon, ob die Methode beim Laden aus einer kompilierten Assembly schneller ausgeführt wird, kompilieren Sie sie einfach in eine Assembly und laden Sie sie! Siehe meine Antwort unter Warum wird Func <> aus Expression <Func <>> langsamer als Func <> direkt deklariert? für mehr Details wie.




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