Ich erinnere mich aus meiner Zeit als Programmierer in C, dass das Betriebssystem beim Verbinden von zwei Zeichenfolgen Speicher für die verknüpfte Zeichenfolge zuweisen muss, dann kann das Programm den gesamten Zeichenfolgentext in den neuen Bereich im Speicher kopieren, dann muss der alte Speicher manuell veröffentlicht werden. Wenn dies also mehrmals erfolgt, wie im Fall des Beitritts zu einer Liste, muss das Betriebssystem ständig mehr und mehr Speicher zuweisen, um ihn nach der nächsten Verkettung freizugeben. Eine viel bessere Möglichkeit, dies in C zu tun, besteht darin, die Gesamtgröße der kombinierten Zeichenfolgen zu bestimmen und den erforderlichen Speicher für die gesamte verknüpfte Liste von Zeichenfolgen zuzuweisen.
In modernen Programmiersprachen (z. B. C #) werden die Inhalte von Sammlungen häufig zusammengefügt, indem die Sammlung durchlaufen und alle Zeichenfolgen einzeln zu einer einzelnen Zeichenfolgenreferenz hinzugefügt werden. Ist das nicht ineffizient, auch bei moderner Rechenleistung?
quelle
Antworten:
Ihre Erklärung, warum es ineffizient ist, ist korrekt, zumindest in den Sprachen, mit denen ich vertraut bin (C, Java, C #), obwohl ich nicht zustimmen würde, dass es allgemein üblich ist, massive Mengen an String-Verkettung durchzuführen. In dem C # -Code, an dem ich arbeite
StringBuilder
,String.Format
wird häufig usw. verwendet. Dies sind alles speichersparende Techniken, um eine Überzuweisung zu vermeiden.Um zur Antwort auf Ihre Frage zu gelangen, müssen wir eine andere Frage stellen: Wenn es nie wirklich ein Problem ist, Zeichenfolgen zu verketten, warum möchten
StringBuilder
undStringBuffer
existieren Klassen ? Warum ist die Verwendung solcher Klassen auch in Programmierbüchern und -klassen für Anfänger enthalten? Warum sollten scheinbar ausgereifte Optimierungsratschläge so wichtig sein?Wenn die meisten Entwickler, die Zeichenfolgen verketten, ihre Antwort nur auf Erfahrung stützen würden, würden die meisten sagen, dass dies niemals einen Unterschied macht, und würden die Verwendung solcher Tools zugunsten der "besser lesbaren" vermeiden
for (int i=0; i<1000; i++) { strA += strB; }
. Aber sie haben es nie gemessen.Die eigentliche Antwort auf diese Frage findet sich in dieser SO-Antwort , aus der hervorgeht, dass in einem Fall beim Verketten von 50.000 Zeichenfolgen (die je nach Anwendung häufig vorkommen) selbst kleine Zeichenfolgen zu einem 1000- fachen Leistungseinbruch führten .
Wenn Leistung buchstäblich überhaupt nichts bedeutet, verketten Sie sie auf jeden Fall. Aber ich würde nicht zustimmen , dass Alternativen (String) schwierig oder weniger lesbar mit , und daher wäre eine vernünftige Vorgehensweise zu programmieren sein , dass sollte nicht um die „vorzeitige Optimierung“ Verteidigung.
AKTUALISIEREN:
Ich denke, es kommt darauf an, Ihre Plattform zu kennen und ihre Best Practices zu befolgen, die leider nicht universell sind . Zwei Beispiele aus zwei verschiedenen "modernen Sprachen":
Es ist nicht gerade eine Hauptsünde, nicht jede Nuance jeder Plattform sofort zu kennen, aber wichtige Plattformprobleme wie diese zu ignorieren, wäre fast so, als würde man von Java zu C ++ wechseln und sich nicht um die Freigabe von Speicher kümmern.
quelle
strA + strB
ist genau das gleiche wie mit einem StringBuilder. Es hat einen 1x Performance-Hit. Oder 0x, je nachdem, wie Sie messen. Für weitere InformationenEs ist ungefähr aus den von Ihnen beschriebenen Gründen nicht effizient. Zeichenfolgen in C # und Java sind unveränderlich. Operationen an Zeichenfolgen geben eine separate Instanz zurück, anstatt die ursprüngliche zu ändern, anders als in C. Wenn mehrere Zeichenfolgen verkettet werden, wird bei jedem Schritt eine separate Instanz erstellt. Das Zuweisen und spätere Sammeln dieser nicht verwendeten Instanzen durch Müll kann zu Leistungseinbußen führen. Nur dieses Mal wird die Speicherverwaltung für Sie vom Garbage Collector übernommen.
Sowohl C # als auch Java führen eine StringBuilder-Klasse als veränderbare Zeichenfolge speziell für diese Art von Aufgaben ein. Ein Äquivalent in C würde eine verknüpfte Liste verketteter Zeichenfolgen verwenden, anstatt sie in einem Array zu verbinden. C # bietet auch eine praktische Join-Methode für Strings zum Verbinden einer Sammlung von Strings.
quelle
Genau genommen ist die Nutzung von CPU-Zyklen weniger effizient, sodass Sie richtig liegen. Aber was ist mit Entwicklerzeit, Wartungskosten usw. Wenn Sie die Zeitkosten zur Gleichung hinzufügen, ist es fast immer effizienter, das zu tun, was am einfachsten ist, und bei Bedarf die langsamen Bits zu profilieren und zu optimieren.
"Die erste Regel der Programmoptimierung: Tun Sie es nicht. Die zweite Regel der Programmoptimierung (nur für Experten!): Tun Sie es noch nicht."
quelle
Ohne einen praktischen Test ist es sehr schwer, etwas über die Leistung zu sagen. Kürzlich war ich sehr überrascht, als ich herausfand, dass in JavaScript eine naive String-Verkettung normalerweise schneller war als die empfohlene Lösung "Liste erstellen und beitreten" ( hier testen , t1 mit t4 vergleichen). Ich bin immer noch verwirrt darüber, warum das passiert.
Einige Fragen, die Sie möglicherweise stellen, wenn Sie über die Leistung (insbesondere über die Speichernutzung) nachdenken, sind: 1) Wie groß ist meine Eingabe? 2) Wie schlau ist mein Compiler? 3) Wie verwaltet meine Laufzeit den Speicher? Dies ist nicht erschöpfend, aber es ist ein Ausgangspunkt.
Wie groß ist mein Input?
Eine komplexe Lösung hat häufig einen festen Overhead, möglicherweise in Form von zusätzlichen Operationen, die ausgeführt werden müssen, oder möglicherweise in Form von zusätzlichem Speicher, der benötigt wird. Da diese Lösungen für große Fälle ausgelegt sind, haben die Implementierer normalerweise kein Problem damit, diese zusätzlichen Kosten einzuführen, da der Nettogewinn wichtiger ist als die Mikrooptimierung des Codes. Wenn Ihre Eingabe ausreichend klein ist, hat eine naive Lösung möglicherweise eine bessere Leistung als die komplexe, wenn auch nur, um diesen Overhead zu vermeiden. (zu bestimmen, was "ausreichend klein" ist, ist jedoch der schwierige Teil)
Wie schlau ist mein Compiler?
Viele Compiler sind intelligent genug, um Variablen, die geschrieben, aber nie gelesen werden, zu "optimieren". Ebenso kann ein guter Compiler möglicherweise eine naive Zeichenfolgenverkettung in eine (Kern-) Bibliotheksverwendung konvertieren. Wenn viele von ihnen ohne Lesevorgänge ausgeführt werden, muss sie zwischen diesen Vorgängen nicht wieder in eine Zeichenfolge konvertiert werden (auch wenn Ihr Quellcode scheint genau das zu tun. Ich kann nicht sagen, ob oder in welchem Umfang Compiler dies tun oder nicht (AFAIK Java ersetzt mindestens mehrere Concats im selben Ausdruck durch eine Folge von StringBuffer-Operationen), aber es ist eine Möglichkeit.
Wie verwaltet meine Laufzeit den Speicher?
In modernen CPUs ist der Engpass normalerweise nicht der Prozessor, sondern der Cache. Wenn Ihr Code in kurzer Zeit auf viele "entfernte" Speicheradressen zugreift, überwiegt die Zeit, die benötigt wird, um den gesamten Speicher zwischen den Cache-Ebenen zu verschieben, die meisten Optimierungen in den verwendeten Anweisungen. Dies ist besonders wichtig bei Laufzeiten mit Garbage Collectors der Generation, da sich die zuletzt erstellten Variablen (z. B. innerhalb desselben Funktionsumfangs) normalerweise in zusammenhängenden Speicheradressen befinden. Diese Laufzeiten verschieben den Speicher auch routinemäßig zwischen Methodenaufrufen hin und her.
Eine Möglichkeit, die Verkettung von Zeichenfolgen zu beeinflussen (Haftungsausschluss: Dies ist eine wilde Vermutung, ich bin nicht sicher genug, um dies mit Sicherheit zu sagen), wäre, wenn der Speicher für die naive Person nahe dem Rest des Codes zugewiesen würde, der ihn verwendet (sogar) Wenn es mehrmals zugewiesen und freigegeben wird), während der Speicher für das Bibliotheksobjekt weit davon entfernt zugewiesen wurde (daher ändern sich die vielen Kontextänderungen, während Ihr Code berechnet, die Bibliothek verbraucht, Ihr Code mehr berechnet usw., und es entstehen viele Cache-Fehler). Natürlich werden bei großen Eingaben OTOH die Cache-Fehler trotzdem auftreten, so dass das Problem der Mehrfachzuweisungen stärker wird.
Trotzdem befürworte ich nicht die Verwendung dieser oder jener Methode, sondern nur, dass Tests, Profilerstellung und Benchmarking jeder theoretischen Analyse der Leistung vorausgehen sollten, da die meisten Systeme heutzutage einfach zu komplex sind, um sie ohne tiefes Fachwissen zu verstehen.
quelle
StringBuilder
unter der Haube verwendet. Alles, was er tun müsste, ist, nicht aufzurufen,toString
bis die Variable tatsächlich benötigt wird. Wenn ich mich richtig erinnere, geschieht dies für einen einzelnen Ausdruck. Mein einziger Zweifel ist, ob er für mehrere Anweisungen in derselben Methode gilt oder nicht. Ich weiß nichts über .NET-Interna, aber ich glaube, dass der C # -Compiler auch eine ähnliche Strategie anwenden könnte.Joel hat vor einiger Zeit einen großartigen Artikel zu diesem Thema geschrieben. Wie einige andere betont haben, ist es stark von der Sprache abhängig. Aufgrund der Art und Weise, wie Zeichenfolgen in C implementiert werden (nullterminiert, ohne Längenfeld), ist die Standardroutine der strcat-Bibliothek sehr ineffizient. Joel präsentiert eine Alternative mit nur einer kleinen Änderung, die viel effizienter ist.
quelle
Nein.
Haben Sie "Die traurige Tragödie des Mikrooptimierungstheaters" gelesen ?
quelle