Große Textmengen in einem String zu speichern, ist möglich, aber nicht immer empfehlenswert. Besonders beim Laden und Bearbeiten sehr großer Textdateien wird schnell deutlich, dass diese Vorgänge unverhältnismäßig lange dauern können.
Vor vielen Jahren stieß ich auf eine Funktion, die jemand geschrieben hatte, um eine 5 MiB große Textdatei einzulesen und den Inhalt als String zurückzugeben. Nachdem ich meinen Kaffee getrunken hatte, war auch die Funktion endlich fertig. Ich empfand dies als ineffizient, da der Prozessor dabei sehr stark ausgelastet war. Auch wenn heutige Computer wesentlich schneller sind, bleibt das Einlesen einer solchen Datei, besonders wenn es monatlich geschieht, für den Benutzer mühsam. Dies war besonders problematisch, da es sich um wichtige Zahlungsdaten handelte.
Warum wird das Verarbeiten oder Bearbeiten von Strings mit großem Inhalt langsam?
- Speicherverwaltung:Große Strings benötigen auch mehr Speicherplatz. Dadurch wird die Verwaltung und Allokation des Speichers zeitaufwendiger, besonders wenn der Speicher fragmentiert ist.
- Kopieren von Daten: Wird ein String modifiziert, wird oft zuerst eine Kopie des gesamten Strings erstellt. Je nach Größe des Strings kann dies viel Zeit in Anspruch nehmen. Bei der Modifikation eines einzelnen Zeichens fällt dies nicht auf, aber wenn ein String millionenfach modifiziert wird, bemerkt der Benutzer die Verzögerung deutlich.
- Suche und Manipulation: Wird ein String durchsucht oder manipuliert, dauert es länger, da viele Daten verarbeitet werden müssen. Nehmen wir die 5 MiB große Textdatei als Beispiel: Bei UTF-8-Kodierung entspricht dies etwa 5.242.880 Zeichen (da ein ASCII-Zeichen 1 Byte entspricht). Wenn ich nun das Zeichen
;
durch|
ersetze, was schätzungsweise 1.000.000 Mal in dieser Datei vorkommt, wird jedes Mal der gesamte String im Arbeitsspeicher kopiert, geändert und dann über den Variablennamen verfügbar gemacht. Danach wird das Original (der String vor der Manipulation) gelöscht, und es geht zur nächsten Änderung der 1.000.000 Zeichen. Dies ist also sehr aufwendig. - Cache-Effizienz: Moderne Prozessoren verwenden Caches, um den Zugriff auf häufig verwendete Daten zu beschleunigen. Sehr große Strings passen möglicherweise nicht vollständig in den Cache, was zu häufigeren und langsameren Speicherzugriffen führt.
Moderne Prozessoren verwenden Caches, um den Zugriff auf häufig verwendete Daten zu beschleunigen. Sehr große Strings, wie eine 100 MiB Textdatei, passen möglicherweise nicht vollständig in den Cache, was zu häufigeren und langsameren Speicherzugriffen führt. Wenn beispielsweise das Zeichen ;
durch |
ersetzt wird und dies 1.000.000 Mal in der Datei vorkommt, wird jedes Mal der gesamte String im Arbeitsspeicher kopiert, geändert und dann über den Variablennamen verfügbar gemacht. Danach wird das Original (der String vor der Manipulation) gelöscht, und es geht zur nächsten Änderung der 1.000.000 Zeichen. Dies ist also sehr aufwendig. Allerdings ist heute der Effekt nicht mehr so extrem, wie es mal war. Heutige Computer lesen eine Textdatei von 100MiB schon in 0,06 Sekunden. Auch Änderungen innerhalb des Strings sind heute schnell erledigt, dauer ist ähnlich, der Speicherverbrauch ist kurzzeitig (bei mir) auf knapp über 500MiB. Also bei heutigen Computern kann das Thema zwar zweitrangig betrachtet werden, allerdings ist je nach Anwendung zu bedenken das der/die Programmierer/in Ressourcen schonend arbeiten sollte.
Beispiel
Ganz Ehrlich? Dieses Beispiel ist etwas überzogen, aber dieses Beispiel zeigt den zeitlichen Unterschied.
In diesem Fall erstelle ich ein String mit 7.688.460 Zeichen.
Etwas übertrieben allerdings wird hier der Zeitunterschied richtig sichtbar.
string result = string.Empty ;
int tl = 7688460;
DateTime j = DateTime.Now;
string exampleText = "Beispieltext";
while (result.Length <= tl) {
result += exampleText;
}
DateTime ts = DateTime.FromOADate (DateTime.Now.ToOADate()- j.ToOADate());
label1.Text = "Dauer: " + ts.Minute +":" + ts.Second.ToString() +"," + ts.Millisecond.ToString();
Was macht der obere Code?
Im String `result` wird der Text „Beispieltext“ wiederholt kopiert, bis eine Länge von mindestens 7.688.460 Zeichen erreicht ist. Die Dauer des Vorgangs wird von Beginn bis zum Ende der Methode gemessen. Die Methode operiert hauptsächlich innerhalb einer while-Schleife. Die Variable `tl` definiert die Mindestgröße des Strings.
Es ist eine sehr einfache Methode, mal eben schnell Programmiert. Allerdings dauert diese sehr lang und produziert viel Auslastung, also der Computer hat einiges zu leisten.
Wie oben beschrieben ist diese Methode nicht Ressourcenschonend
Der Prozess zeigt, dass der Prozessor (4 Kerne mit 8 logischen Prozessoren) intensiv arbeitet. Er verbraucht enorme Mengen an Speicher für nur wenige Zeilen Code. Die Methode wurde zwar schnell entwickelt, ist jedoch alles andere als leistungsfähig und sparsam im Ressourcenverbrauch.
Aufgrund der hohen Auslastung des Prozessors durch diesen Prozess reagiert die GUI (Graphical User Interface / Benutzeroberfläche) nicht mehr, und es wird „Keine Rückmeldung“ angezeigt, was den Benutzer dazu verleiten kann, den Prozess zu beenden. Dies führt dazu, dass der Benutzer den Support kontaktiert, was wiederum die Arbeitsbelastung der Mitarbeiter steigert und letztendlich den Programmierer erreicht, was dessen Zeit Planung beeinträchtigen kann.
Diese Methode 1 benötigt bei mir 48 Minuten, 8 Sekunden und 159 Millisekunden. Sehr lang.
Jetzt nehmen wir die selbe Methode dessen Ergebnis auch ein Text mit 7.688.460 Zeichen sein soll, nur als Array.
DateTime j = DateTime.Now;
int targetLength = 7688460;
string exampleText = "Beispieltext";
int exampleTextLength = exampleText.Length;
int arraySize = (int)Math.Ceiling((double)targetLength / exampleTextLength);
string[] stringArray = new string[arraySize];
for (int i = 0; i < arraySize; i++) {
stringArray[i] = exampleText;
}
string result = string.Join("", stringArray);
result = result.Substring(0, targetLength);
DateTime ts = DateTime.FromOADate(DateTime.Now.ToOADate() - j.ToOADate());
label2.Text = "Dauer: " + ts.Minute + ":" + ts.Second.ToString() + "," + ts.Millisecond.ToString();
Was macht der obere Code?
Im String `Stringarray` wird der Text „Beispieltext“ wiederholt kopiert, bis eine Länge von mindestens 7.688.460 Zeichen erreicht ist. Die Dauer des Vorgangs wird von Beginn bis zum Ende der Methode gemessen. Die Methode operiert hauptsächlich innerhalb einer while-Schleife. Die Variable `tl` definiert die Mindestgröße des Strings.
Wie man am Coding 2 sieht ist die Arbeit, gerade für Berufseinsteiger etwas komplexer. Der Programmieraufwand ist etwas höher.
Allerdings ist die Wartezeit für den Anwender um weiten kürzer. Der Benutzer merkt nahezu gar nicht was diese Methode an Arbeit leistet wenn man diese mit dem Coding 1 vergleicht.
Die Methode 2 benötigt 0 Minuten, 0 Sekunden und 23 Millisekunden.
Hier lohnt es sich ein bisschen Arbeit zu investieren. Denn hier erhält der Anwender keine Meldung wie „Keine Rückmeldung“. Der Kunde muss nicht lange warten und der Kunde ruft den Support nicht an. So sind alle zufrieden und können Ihre Arbeit machen.
Mit diesem Blogeintrag möchte ich gerne Zeigen, das wenn etwas mehr Energie in der Programmierung gesteckt wird, der Einfluß des Programmierers auch die Hardwarekosten senken kann. Denn oft sagt der Support bei schlecht Programmierter Software das der Rechner zu langsam sei.
Nur zur Vervollständigung:
Beide Codes wurden in C# entwickelt. Ihr könnt die Codes testen, indem eine Windows Forms-Anwendung mit zwei Buttons und zwei Labels erstellen. Beim Klick event des Button1, wird der erste Code ausgeführt, und bei Button2 der zweite Code. Die gemessenen Zeiten werden dann im Text von Label1 beziehungsweise Label2 angezeigt.
Das MS .Net Framework besitzt auch Möglichkeiten dies zu vereinfachen.
Ich nenne hier nur ein Beispiel mit der Klasse Stringbuilder.
DateTime j = DateTime.Now;
StringBuilder sb = new StringBuilder(7688460);
string exampleText = "Beispieltext";
while (sb.Length + exampleText.Length <= 7688460) {
sb.Append(exampleText);
}
string result = sb.ToString();
DateTime ts = DateTime.FromOADate(DateTime.Now.ToOADate() - j.ToOADate());
label3.Text = "Dauer: " + ts.Minute + ":" + ts.Second.ToString() + "," + ts.Millisecond.ToString();
Was macht Code
Diese Methode ergibt das gleiche aus wie die anderen beiden, die einzige Änderung ist das der Stringbuilder verwendet wird.
Auch hier erhält der Anwender keine Meldung wie „Keine Rückmeldung“. Der Kunde muss nicht lange warten und der Kunde ruft den Support nicht an. So sind alle zufrieden und können Ihre Arbeit machen.
Hier macht .NET die meiste Arbeit. unter sb.Append() passiert eigentlich gleiches wie in Code Listening 2. Nur Einsteiger tuhen es sich oft schwer mit Arrays zu Arbeiten. Auszubildende arbeiten anfangs gerne mit Listen oder Collections.