Laden...

Geschwindigkeit : Speicher Allokierung für String Variablen

Erstellt von Charly vor 4 Jahren Letzter Beitrag vor 4 Jahren 2.216 Views
C
Charly Themenstarter:in
31 Beiträge seit 2014
vor 4 Jahren
Geschwindigkeit : Speicher Allokierung für String Variablen

Hallo,

ich habe hier ein Problem was ich gelöst habe, jedoch interessieren mich dazu andere Möglichkeiten und Meinungen wie man damit umgehen könnte.

Ich lese sehr viele Daten und baue daraus eine CSV Datei die ich wegschreibe. Erster Ansatz war : "Wird im Arbeitsspeicher wohl am schnellsten sein ..." , also lasse ich alle Logikroutinen rüberrattern und erzeuge einen String der die gesamte CSV Datei enthält. Nun ist es wohl so, dass C# den Speicherbedarf für die Variable bei jedem Anhängen von weiteren Daten immer neu alloziert. Dieses hin und her kostet massivst Zeit. Nach 4 Minuten war erst ein Drittel der Verarbeitung durch.

Also habe ich einen Streamwriter genommen und jede Zeile stumpf auf die Festplatte geschrieben. Alle 110.000 Datensätze werden in 1-2 Sekunden geschrieben.

Die Logik dahinter - worüber ich im ersten Ansatz nicht nachgedacht habe - warum das so ist leuchtet mir ein.

Meine Gedanken:

  1. Prinzipiell ist RAM schneller als HDD ...
  2. Natürlich kann auch mit Dateien arbeiten, aber warum sollte ich gegen die Logik aus 1. arbeiten ?
  3. Kann man für die String Variable nicht gleich vorbestimmen wieviel Speicher sie von vornherein allozieren soll ? (Eventuell über den StringBuilder ?)
  4. Alloziert man selbst Speicher und schreibt darin mit einem Pointer weg ? (unsafe) der Bedarf könnte vorab Bytegenau berechnet werden - wäre wohl das schnellste denke ich ?
  5. Memorystream erzeugen - kann man da eine Speichergröße vorallozieren um nicht unsafe selbst Speicher reservieren zu müssen ?

Nehmen wir mal an dass wir das direkt über den RAM lösen möchten. Über welchen Weg würdet Ihr das lösen ?

Gruß
Charles

T
2.219 Beiträge seit 2008
vor 4 Jahren

Dein Problem dürfte sich beim umstellen von String auf den StringBuilder lösen.
Strings erzeugen bei allen Operationen immer eine neue Instanz mit dem neuen Wert, was beim generieren von einer größeren CSV den Speicher vollmüllt und eben lange dauert.

Beim StringBuilder wird der String erst beim Aufruf von ToString() erzeugt.
Da kannst du munter fröhlich Daten anhängen und beim schreiben der Datei einfach ToString aufrufen.

Nachtrag:
Deine Denkansätze kannst du übrigens durch den StringBuilder verwerfen.
Es gibt sogut wie nie einen Grund String Operationen durch unsafe oder andere Konstrukte laufen zu lassen.
Mal davon abgesehen, dass du damit dann wieder Code schreibst, den dir StringBuilder/String schon bieten.

StringBuilder

T-Virus

Developer, Developer, Developer, Developer....

99 little bugs in the code, 99 little bugs. Take one down, patch it around, 117 little bugs in the code.

C
Charly Themenstarter:in
31 Beiträge seit 2014
vor 4 Jahren

Ich lasse das mal interesse halber für mich in der Schleife durchlaufen und prüfe die Schnelligkeit der StringBuilder Routine. Wenn er einfach nur "hinten dran" Speicher neu allokiert dürfte das ja kein Problem sein. Das ständig neu Instanzieren geht (natürlich) ganz schön auf die Performance.

Bin gespannt.

*EDIT*

Ich habe nur gefühlt gemessen und nicht in Milisekunden : gefühlt ist der StringBuilder aber ungefähr genau so schnell wie das direkte Schreiben auf Festplatte und damit die beste Methode das Problem zu lösen. Ich könnte jetzt aus dem Gefühl nicht sagen welche Methode schneller wäre - wobei 1-2 Sekunden gegenüber 12 Minuten ein erheblicher Geschwindigkeitsgewinn ist.


buffer += "DATEN;DATEN;DATEN;DATEN;DATEN;";

ist dahingehend nicht zu empfehlen ...

Wieder was gelernt ... es wird zwar nicht direkt neu instanziert, aber das was im Hintergrund abläuft ist halt nicht performant.

4.931 Beiträge seit 2008
vor 4 Jahren

Du solltest aber dann dem StringBuilder eine Anfangsgröße mitgeben, damit intern nicht immer wieder neu Speicher alloziert werden muß (das ja auch Performance kostet).
Wenn du in etwa die Länge jeder Datenzeile kennst, dann multipliziere diese mit der Anzahl der Datensätze, um die ungefähre Gesamtgröße vorab zu bestimmen (du kannst ja auch noch einen kleinen Puffer draufaddieren).

Es spricht aber nichts dagegen, die Daten auch gleich auf Platte zu schreiben (außer dein Programm arbeitet nachher noch mit den Daten weiter und müßte diese dann erst wieder laden).

T
2.219 Beiträge seit 2008
vor 4 Jahren

@Charly
Wie verwendest du den aktuell den StringBuilder?
Hier solltest du z.B. auch mit AppendFormat arbeiten, wenn du die Werte im StringBuilder einfügst.
Wenn du auch hier wieder den String beim Append/AppendLine mit + verknüpfst, werden auch wieder einzelne String Instanzen erzeugt bei jeder Zeile.
Auch das kannst du vermutlich noch weg elimieren.

Dann dürftest du auch die performanteste Umsetzung haben.
Das wegschreiben, kannst du dann auch nicht mehr optimieren.

T-Virus

Developer, Developer, Developer, Developer....

99 little bugs in the code, 99 little bugs. Take one down, patch it around, 117 little bugs in the code.

C
Charly Themenstarter:in
31 Beiträge seit 2014
vor 4 Jahren

Eine Anfangsgröße hatte ich nicht mitgegeben ; das könnte das ganze noch etwas performanter machen ; aber prinzipiell war das schon ok. Ich habe den Datensatz mit .Append einfach hinzugefügt.

Danke euch für den Austausch ...

6.911 Beiträge seit 2009
vor 4 Jahren

Hallo Charly,

bei jedem Anhängen von weiteren Daten immer neu alloziert

siehe [FAQ] Besonderheiten der String-Klasse (immutabler Referenztyp mit Wertsemantik)

Kann man für die String Variable nicht gleich vorbestimmen wieviel Speicher sie von vornherein allozieren soll ?

Direkt: Ab .NET Core 2.1 mittels string.Create. Achtung: die Länge muss genau angegeben werden, da so direkt in den Speicher vom (dann) fertigen String geschrieben wird.

Indirekt: via StringBuilder wie hier schon vorgeschlagen.

Alloziert man selbst Speicher und schreibt darin mit einem Pointer weg ? (unsafe) der Bedarf könnte vorab Bytegenau berechnet werden - wäre wohl das schnellste denke ich ?

Nicht nötig, umständlich und fehleranfällig.

Memorystream erzeugen - kann man da eine Speichergröße vorallozieren um nicht unsafe selbst Speicher reservieren zu müssen ?

Ja, siehe :rtfm:

Wenn er einfach nur "hinten dran" Speicher neu allokiert dürfte das ja kein Problem sein.

Im StringBuilder gibt es einen Buffer für die Zeichen des zu erstellenden Strings. Wenn der Buffer durch die Appends voll wird, so wird ein doppelt so großer Buffer* erstellt, der Inhalt des bisherrigen Buffers in den neuen Buffer kopiert und der bisherige Buffer verworfen (bis in der GC abräumt).
Daher auch der Hinweis von Th69: Anfängsgröße bekanntgeben, das spart Buffer-Wachstum wie vorhin beschrieben.

* kann sein dass sich das geändert hat und eine andere Wachstumsrate verwendet wird, das ändert aber nichts an der Aussage

Es spricht aber nichts dagegen, die Daten auch gleich auf Platte zu schreiben

👍
Das sollte auch gemacht werden, denn es bringt nichts zuerst im RAM einen großen String zusammenzubauen, der dann fürs Schreiben in die Datei kodiert (z.B. UTF-8) werden muss um dann in den Datei-Buffer zu schreiben, ...
Das kann auch direkt geschrieben werden, z.B. mittels StreamWriter der auch Methoden fürs formatierte Schreiben bietet.

Dann gibt es auch keinen Riesenstring der vermutlich im LOH (large object heap) landet (falls er mehr als 85e3 bytes hat) und erst mit einer Gen2-Collection des GC abgeräumt wird -- also sehr selten.
Beim direkten Schreiben können zwar mehrere Gen0-Collections vom GC auftreten, aber die sind nicht schlimm.

Dann dürftest du auch die performanteste Umsetzung haben.
Das wegschreiben, kannst du dann auch nicht mehr optimieren.

Ich fürchte das geht jetzt zuweit, aber der Vollständigkeithalber:
Vermutlich geht es mit IO.Pipelines noch performanter und v.a. mit weniger Allokationen.
ABER: der Code wird dadurch aufwändiger und umständlicher (= weniger wartbar). Sollte es keine Server-Lösung od. keine zwingende Forderung nach schnellstem Code geben, so soll wartbarer Code bevorzugt werden und keine Suche nach ultimativer Performance gestartet werden.

Ich würde mit dem StreamWriter direkt schreiben. Ist einfach umzusetzen, performant und der Code leserlich.

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

3.003 Beiträge seit 2006
vor 4 Jahren

Übrigens ist bei deiner ursprünglichen Variante nicht die Speicherallokation teuer, sondern das kopieren der Strings, das notwendig ist, weil string nun einmal immutable ist.

LaTino

"Furlow, is it always about money?"
"Is there anything else? I mean, how much sex can you have?"
"Don't know. I haven't maxed out yet."
(Furlow & Crichton, Farscape)

6.911 Beiträge seit 2009
vor 4 Jahren

Hallo,

nicht die Speicherallokation teuer

Das ist als generelle Antwort / Hinweis zu sehen.

Speicherallokationen auf dem Heap (per new od. implizit wie beim Kopieren vom String) sind in .NET nicht aufwändig. Im Grunde wird vom GC nur ein Pointer verschoben und der so allozierte Speicher genullt (aufgrund von ".NET safety"). Das geht schnell.
Aufwändig hingegen ist das Abräumen vom nicht mehr benötigten Speicher durch den GC -- das sammeln und kompaktieren vom Speichern und ev. das verschieben der Objekte in die nächste Generation.
D.h. wenn Speicherallokaitonen vermieden werden können, so erspart man dem GC eine Menge Arbeit und die Anwendung wird insgesamt schneller. Bemerkbar vllt. nicht unbedingt bei Konsolenanwendungen die nur ein Job ausführen und sich beenden, sondern v.a. bei Server-Anwendungen u.ä. Anwendungen die "lange" laufen.

Speicherallokationen können oft vermieden werden durch* geschicktere Algorithmen / geschicktere Verwendung von BCL-Typen

  • Verwendung vom ArrayPool<T>, mit dem sich die Speicherallokationen amortisieren lassen
    Alternativ auch MemoryPool<T>, etc. Generell durch Poolen von Objekten die häufig verwendet werden

  • Stack-Allokationen mittels stackalloc -- vorzugsweise in Verbindung mit Span<T>, denn hier ist das Allozieren und Deallozieren vom Speichern nur eine Pointer-Operation (und das Nullen vom Allozierten Speicher), d.h. sehr schnell

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"