2008-10-19 3 views
13

Es passiert mir nur mit einer Frage zum Code-Design. Sagen wir, ich habe eine "Template" -Methode, die einige Funktionen aufruft, die sich "ändern" können. Ein intuitives Design folgt "Template Design Pattern". Definieren Sie die Änderungsfunktionen als "virtuelle" Funktionen, die in Unterklassen überschrieben werden sollen. Oder ich kann Delegiertenfunktionen einfach ohne "virtuell" verwenden. Die Delegate-Funktionen sind injiziert, so dass sie auch angepasst werden können.C#: Virtual Function-Aufruf ist sogar schneller als ein Delegat-Aufruf?

Ursprünglich dachte ich, der zweite "Delegierten" Weg wäre schneller als "virtueller" Weg, aber einige Codeschnipsel beweist, dass es nicht korrekt ist.

Im folgenden Code folgt die erste DoSomething-Methode "Vorlagenmuster". Es ruft die virtuelle Methode IsTokenChar auf. Die zweite DoSomthing-Methode hängt nicht von der virtuellen Funktion ab. Stattdessen hat es einen Pass-In-Delegaten. In meinem Computer ist das erste DoSomthing immer schneller als das zweite. Das Ergebnis ist wie 1645: 1780.

"Virtueller Aufruf" ist dynamische Bindung und sollte mehr Zeit kosten als direkter Aufruf der Delegation, nicht wahr? aber das Ergebnis zeigt, dass es nicht ist.

Jeder kann das erklären?

using System; 
using System.Diagnostics; 

class Foo 
{ 
    public virtual bool IsTokenChar(string word) 
    { 
     return String.IsNullOrEmpty(word); 
    } 

    // this is a template method 
    public int DoSomething(string word) 
    { 
     int trueCount = 0; 
     for (int i = 0; i < repeat; ++i) 
     { 
      if (IsTokenChar(word)) 
      { 
       ++trueCount; 
      } 
     } 
     return trueCount; 
    } 

    public int DoSomething(Predicate<string> predicator, string word) 
    { 
     int trueCount = 0; 
     for (int i = 0; i < repeat; ++i) 
     { 
      if (predicator(word)) 
      { 
       ++trueCount; 
      } 
     } 
     return trueCount; 
    } 

    private int repeat = 200000000; 
} 

class Program 
{ 
    static void Main(string[] args) 
    { 
     Foo f = new Foo(); 

     { 
      Stopwatch sw = Stopwatch.StartNew(); 
      f.DoSomething(null); 
      sw.Stop(); 
      Console.WriteLine(sw.ElapsedMilliseconds); 
     } 

     { 
      Stopwatch sw = Stopwatch.StartNew(); 
      f.DoSomething(str => String.IsNullOrEmpty(str), null); 
      sw.Stop(); 
      Console.WriteLine(sw.ElapsedMilliseconds); 
     } 
    } 
} 
+2

One für Jon Skeet, fühle ich mich! ;) –

+0

@Mitch: Ich hatte deinen Kommentar vor der Beantwortung noch nicht gesehen, aber ich fühle mich geschmeichelt :) –

+0

Btw, ich finde den Unterschied besser mit einem optimierten Build markiert zu sein –

Antwort

1

Es ist möglich, dass da Sie keine Methoden, die die virtuelle Methode überschreiben, dass der JIT der Lage ist, dies zu erkennen und einen direkten Aufruf stattdessen zu verwenden.

Für so etwas ist es im Allgemeinen besser, es zu testen, als Sie getan haben, als zu versuchen, die Leistung zu erraten. Wenn Sie mehr darüber erfahren möchten, wie der Aufruf von Delegaten funktioniert, empfehle ich das ausgezeichnete Buch "CLR Via C#" von Jeffrey Richter.

+0

Nicht nur das, sondern auch wenn man Delegierte speichert/anruft waren effizienter als der virtuelle Versand, würde das Framework fast sicher speichern und Delegaten anrufen, anstatt virtuellen Versand zu verwenden. Da dies nicht der Fall ist, ist es wahrscheinlich nicht (außer in seltenen/pathologischen Fällen). – technophile

+0

Ich denke nicht, dass es einen Unterschied macht, die Methode ist "virtuell" in der Elternklasse oder "überschreiben" in der Unterklasse. Wenn es "virtuell" ist, ist es immer dynamisch bindend. –

-1

virtuelle Überschreibungen haben eine Art Umleitungstabelle oder etwas, das zur Kompilierungszeit fest codiert und vollständig optimiert ist. Es ist in Stein gemeißelt, sehr schnell.

Delegierte sind dynamisch, die immer einen Overhead haben und sie scheinen auch Objekte zu sein, so dass sich summiert.

Sie sollten sich keine Gedanken über diese kleinen Leistungsunterschiede machen (es sei denn, die Entwicklung von leistungskritischer Software für das Militär), für die meisten Zwecke gewinnt eine gute Code-Struktur die Optimierung.

+0

Schnellkorrektur: Ich denke du meintest "virtuelle Überschreibungen" statt "virtuelle Überladungen" –

+0

Ich lese das als "virtuelle Overlords" das erste Mal. – Andrew

+0

Whoops. Also mein Intellisense-Missbrauch zeigt! –

1

Ich bezweifle, dass es für all Ihren Unterschied, aber eine Sache von der Spitze meines Kopfes, die einige der Unterschied ausmachen kann, ist, dass die virtuelle Methode Versand hat bereits die this Zeiger bereit zu gehen. Beim Aufruf über einen Delegaten muss der this Zeiger vom Delegaten abgerufen werden.

Beachten Sie, dass laut this blog article der Unterschied in .NET v1.x noch größer war.

8

Ein virtueller Aufruf ist der Dereferenzierung von zwei Zeigern bei einem bekannten Offset im Speicher. Es ist nicht wirklich dynamische Bindung; Zur Laufzeit gibt es keinen Code, der über die Metadaten reflektiert, um die richtige Methode zu finden. Der Compiler generiert basierend auf diesem Zeiger einige Anweisungen zum Ausführen des Aufrufs. Tatsächlich ist der virtuelle Aufruf ein einzelner IL-Befehl.

Ein Prädikataufruf erstellt eine anonyme Klasse zum Einkapseln des Prädikats. Diese Klasse muss instanziiert werden und es wird ein Code generiert, um tatsächlich zu überprüfen, ob der Prädikatfunktionszeiger Null ist oder nicht.

Ich würde vorschlagen, Sie betrachten die IL-Konstrukte für beide. Kompilieren Sie eine vereinfachte Version Ihrer Quelle oben mit einem einzelnen Aufruf für jedes der zwei DoSomthing.Dann verwenden Sie ILDASM, um zu sehen, was der tatsächliche Code für jedes Muster ist.

(Und ich bin sicher, dass ich für nicht mit der richtigen Terminologie downvoted bekommen :-))

+0

In meinem Verständnis, "die tatsächliche Methode, um zur Laufzeit entschieden wird" heißt "dynamische Bindung". –

+0

Ein Prädikat ist nur eine Delegateninstanz. Es ist eine Instanz, aber ich bin mir nicht sicher, ob eine "anonyme Klasse" erstellt wurde. Gibt es in C# /. NET ein "anonymes Klassenkonzept"? –

+0

Ein virtueller Aufruf ist nicht "Entscheidung, welche Methode zur Laufzeit aufgerufen wird". Die aufzurufende Methode ist bekannt und der Klasse bereits zugeordnet. –

19

Denken Sie darüber nach, was in jedem Fall erforderlich ist:

Virtuelles Call

  • Auf Nullheit prüfen
  • Navigieren vom Objektzeiger zum Typzeiger
  • Loo k up Methode Adresse in Anweisungstabelle
  • (Nicht sicher - auch Richter deckt dies nicht ab) Gehen Sie zum Basistyp, wenn die Methode nicht überschrieben wird? Recurse bis wir die richtige Methodenadresse gefunden haben. (Ich glaube nicht -. Sehen bearbeiten unten)
  • Drücken ursprünglichen Objektzeiger auf den Stapel ("this")
  • Call-Methode

Delegierter Anruf

  • Auf Nullität prüfen
  • Navigieren vom Objektzeiger zum Array von Aufrufen (alle Delegaten sind möglicherweise Multicast)
  • Schleife über Array und für jeden Aufruf:
    • Fetch Methode Adresse
    • Arbeitsgeführt, ob oder nicht das Ziel als erstes Argument
    • Push-Argumente auf dem Stapel passieren (bereits getan worden sein kann - nicht sicher)
    • gegebenenfalls (je nachdem, ob der Aufruf ist offen oder geschlossen) schiebt den Aufruf Ziel auf den Stapel
    • Call-Methode

Möglicherweise gibt es eine Optimierung, so dass im Einzelanruf keine Schleife auftritt, aber auch dies wird sehr schnell überprüft.

Aber im Grunde gibt es genauso viel Indirektion mit einem Delegierten beteiligt. Angesichts des Bits, auf das ich beim Aufruf der virtuellen Methode nicht weiß, ist es möglich, dass ein Aufruf einer nicht überschriebenen virtuellen Methode in einer hierarchischen Hierarchie mit massivem Tiefgang langsamer ist ... Ich werde es versuchen und mit der Antwort bearbeiten.

EDIT: Ich habe versucht, sowohl mit der Tiefe der Vererbungshierarchie (bis zu 20 Ebenen), den Punkt "am meisten abgeleitetes Überschreiben" und den deklarierten Variablentyp zu spielen - und keiner von ihnen scheint einen Unterschied zu machen.

BEARBEITEN: Ich habe gerade das ursprüngliche Programm mit einer Schnittstelle (die übergeben wird) versucht - das hat etwa die gleiche Leistung wie der Delegat.

+0

Bei virtuellen Anrufen gibt es keine Überprüfung auf Null. Außerdem wird die vtable der Methoden zum Zeitpunkt der Kompilierung ermittelt, sodass keine Run-Time-Rekursion über Basisklassen stattfindet. Der Compiler generiert den Zeiger auf die richtige Methode aus der richtigen Basisklasse und fügt sie in den richtigen Slot in der vtable ein. –

+6

callvirt * prüft * auf null - siehe Abschnitt 4.2 von Partition 3 in der CLI-Spezifikation oder P166 von CLR über C#. (Wenn die Referenz null wäre, welche Implementierung würde aufgerufen?) Danke für die Bestätigung des Bits "no recursion". Das war es, was das Experiment im Grunde vorschlug. –

+3

+1 für die Nicht-Rekursion, Vtables werden beim Kompilierungstyp abgeflacht. – thinkbeforecoding

12

Ich wollte nur ein paar Korrekturen hinzufügen, um John Skeet Antwort:

Ein virtueller Methodenaufruf benötigt keine Nullprüfung (automatisch behandelt mit Hardware-Fallen) zu tun.

Es muss auch nicht die Vererbungskette durchlaufen, um nicht überschriebene Methoden zu finden (dafür steht die virtuelle Methodentabelle).

Ein virtueller Methodenaufruf ist im Wesentlichen eine zusätzliche Ebene der Indirektion beim Aufruf. Es ist langsamer als ein normaler Aufruf wegen der Tabellensuche und des nachfolgenden Funktionszeigeraufrufs.

Ein Delegat-Aufruf beinhaltet auch eine zusätzliche Ebene der Indirektion.

Bei Aufrufen zu einem Delegaten werden keine Argumente in ein Array eingefügt, es sei denn, Sie führen einen dynamischen Aufruf mit der DynamicInvoke-Methode durch.

Ein Delegat-Aufruf umfasst die aufrufende Methode, die eine vom Compiler generierte Invoke-Methode für den betreffenden Delegatentyp aufruft. Ein Aufruf an den Prädikator (Wert) wird in einen Prädikator umgewandelt. Invoke (Wert).

Die Invoke-Methode wiederum wird vom JIT implementiert, um den/die Funktionszeiger aufzurufen (intern im Delegate-Objekt gespeichert).

In Ihrem Beispiel sollte der von Ihnen übergebene Delegat als eine Compiler-generierte statische Methode implementiert worden sein, da die Implementierung nicht auf Instanzvariablen oder Locals zugreift. Daher sollte der Zugriff auf den "This" -Zeiger vom Heap aus nicht erfolgen ein Problem.

Der Leistungsunterschied zwischen Delegaten und virtuellen Funktionsaufrufen sollte weitgehend derselbe sein und Ihre Leistungstests zeigen, dass sie sehr nahe beieinander liegen.

Der Unterschied könnte auf die Notwendigkeit zusätzlicher Prüfungen + Verzweigungen wegen Multicast zurückzuführen sein (wie von John vorgeschlagen). Ein anderer Grund könnte sein, dass der JIT-Compiler die Delegate.Invoke-Methode nicht inline einbaut und die Implementierung von Delegate.Invoke nicht mit Argumenten sowie der Implementierung bei der Ausführung von Methodenaufrufen funktioniert.