C #: Warum fügt .ToString () Text schneller an einen int in eine Zeichenkette um?

7

Dies ist von C # auf den Punkt gebracht Buch

%Vor%

Allerdings sagt es dann "der Ausdruck i +", "bedeutet, dass wir immer noch wiederholt Strings verketten, was jedoch nur geringe Performance-Kosten verursacht, da Strings klein sind"

Dann heißt es, dass es schneller zu

wird, wenn Sie es auf die folgenden Zeilen ändern %Vor%

Aber warum ist das schneller? Jetzt haben wir einen zusätzlichen Schritt, wo i in eine Zeichenkette umgewandelt wird? Was geht hier wirklich unter die Haube? Es gibt keine weitere Erklärung im Rest des Kapitels.

    
iAteABug_And_iLiked_it 17.08.2013, 22:36
quelle

3 Antworten

15

Die ersten zwei Antworten auf Ihre Frage sind nicht ganz korrekt. Die Anweisung sb.Append(i + ","); ruft i.ToString() nicht auf, was sie tatsächlich tut, ist

%Vor%

Intern ruft es in der Funktion string.Concat ToString() für die beiden übergebenen object s auf. Das wichtigste Leistungsproblem in dieser Anweisung ist (object)i . Dies ist Boxen - Wrapping eines Werttyps innerhalb einer Referenz. Dies ist ein (relativ) beträchtlicher Leistungseinbruch, da zusätzliche Zyklen und Speicherzuweisung erforderlich sind, um etwas zu boxen, und dann ist eine zusätzliche Speicherbereinigung erforderlich.

Sie können dies in der IL des (freigegebenen) kompilierten Codes sehen:

%Vor%

Sehen Sie, dass die erste Zeile ein Aufruf von box ist, gefolgt von einem Aufruf von Concat , der schließlich mit dem Aufruf von Append endet.

Wenn Sie stattdessen i.ToString() aufrufen (siehe unten), verzichten Sie auf das Boxing und den Aufruf string.Concat() .

%Vor%

Dieser Aufruf ergibt die folgende IL:

%Vor%

Beachten Sie, dass kein Boxing und keine String.Concat vorhanden sind. Daher müssen weniger Ressourcen gesammelt und weniger Zyklen beim Boxing verschwendet werden, wobei ein Append() -Aufruf hinzukommt, was relativ viel kostet billiger.

Dies ist der Grund, warum der zweite Satz Code eine bessere Leistung ist.

Sie können diese Idee auf viele andere Dinge ausweiten - überall dort, wo Zeichenfolgen übergeben werden, die Sie einen Werttyp an eine Funktion übergeben, die diesen Typ nicht explizit als Argument verwendet (Aufrufe, die ein object als Argument, wie string.Format() zum Beispiel), ist es eine gute Idee, <valuetype>.ToString() bei der Übergabe eines Werttyparguments aufzurufen.

Als Antwort auf die Frage von Theodoros im Kommentar:

Das Compiler-Team hätte sich sicherlich für eine solche Optimierung entscheiden können, aber ich vermute, dass sie entschieden haben, dass die Kosten (in Bezug auf zusätzliche Komplexität, Zeit, zusätzliche Tests usw.) den Wert einer solchen Änderung nicht wert sind die Investition.

Im Grunde hätten sie sich in einen speziellen Fall verzweigen müssen für Funktionen, die scheinbar auf string s operieren, aber eine Überladung mit object (im Grunde: if (boxing occurs && overload has string) ) bieten. Innerhalb dieses Zweiges müsste der Compiler auch prüfen, ob die object Funktionsüberladung dieselben Dinge tut wie die string Überladung mit der Ausnahme, dass ToString() auf den Argumenten aufgerufen wird - sie muss dies tun, weil ein Benutzer das könnte Erstellen Sie Funktionsüberladungen, in denen eine Funktion ein string und ein anderes ein object benötigt, aber die beiden Überladungen unterschiedliche Aufgaben an den Argumenten ausführen.

Das scheint mir eine Menge Komplexität und Analyse zu sein, um eine kleine Optimierung für einige Funktionen zur String-Manipulation zu machen. Darüber hinaus würde dies mit der Kern-Compiler-Funktion Auflösung Code, der bereits einige sehr genaue Regeln, die Menschen missverstehen die ganze Zeit mistend haben (werfen Sie einen Blick auf eine Reihe von Eric Lipperts Antworten - einige drehen sich um Probleme mit der Funktion Auflösung). Es komplizierter zu machen mit "es funktioniert so, außer wenn Sie diese Situation haben" Typ Regeln ist sicherlich etwas zu vermeiden, wenn die Rückkehr minimal ist.

Die weniger kostspielige und weniger komplexe Lösung besteht darin, die Basisfunktionsauflösungsregeln zu verwenden und den Compiler auflösen zu lassen, indem Sie einen Werttyp (wie int ) in eine Funktion übergeben und die einzige Funktion herausfinden Die passende Signatur ist eine, die object benötigt und eine Box macht. Dann verlassen Sie sich auf Benutzer, um die Optimierung von ToString() zu tun, wenn sie ihren Code profilieren und bestimmen, dass es notwendig ist (oder einfach über dieses Verhalten wissen und es immer tun, wenn sie die Situation treffen, was ich tue).

Eine wahrscheinliche Alternative wäre eine Anzahl von string.Concat Überladungen, die int s, double s usw. (wie string.Concat(int, int) ) benötigen und intern ToString für die Argumente aufrufen wo sie nicht verpackt würden. Das hat den Vorteil, dass die Optimierung in der Klassenbibliothek statt im Compiler stattfindet, aber dann kommen Sie unweigerlich in Situationen, in denen Sie Typen in der Verkettung mischen wollen, wie die ursprüngliche Frage hier, wo Sie string.Concat(int, string) haben. Die Permutationen würden explodieren, was wahrscheinlich der Grund ist, warum sie dies nicht getan haben. Sie hätten auch die am häufigsten verwendeten Situationen bestimmen können, in denen solche Überladungen verwendet werden und die Top 5 tun, aber ich vermute, dass sie entschieden haben, dass sie sie nur für Leute öffnen würden, die fragen: "Nun, du hast (int, string) , warum nicht?" t tust du (string, int) ? ".

    
Gjeltema 18.08.2013, 01:29
quelle
4
  

Jetzt haben wir einen zusätzlichen Schritt, wo ich in eine Zeichenkette umgewandelt werde?

Es ist kein zusätzlicher Schritt. Selbst im ersten Snippet muss natürlich die Ganzzahl i in eine Zeichenkette irgendwo konvertiert werden - dies wird vom Additionsoperator erledigt, also passiert es, wo Sie es nicht sehen, aber es passiert immer noch.

Der Grund dafür, dass das zweite Snippet schneller ist, ist, dass es keine neue Zeichenfolge erstellen muss, indem das Ergebnis von i.ToString() und "," verknüpft wird.

Hier ist, was die erste Version macht:

%Vor%
  1. Rufen Sie i.ToString .
  2. auf
  3. Erstellen Sie ein neues string (think new string(iAsString + ",") ).
  4. Rufen Sie sb.Append.
  5. auf

Hier ist, was die zweite Version macht:

  1. Rufen Sie i.ToString .
  2. auf
  3. Rufen Sie sb.Append .
  4. auf
  5. Rufen Sie sb.Append .
  6. auf

Wie Sie sehen können, ist der einzige Unterschied der zweite Schritt, bei dem der Aufruf von sb.Append in der zweiten Version schneller ist als das Verketten zweier Strings und das Erstellen einer anderen Instanz aus dem Ergebnis.

    
Jon 17.08.2013 22:39
quelle
0

Wenn Sie Folgendes tun:

%Vor%

Die zweite tatsächliche Zeile endet mit dem Verlassen der ersten mit "abc" bewerteten Zeichenfolge und erstellt eine neue Zeichenfolge für x="abcd"; Ich denke, das ist der Performance-Hit, den Sie sehen.

    
JBrooks 17.08.2013 22:48
quelle

Tags und Links