Vorsicht bei umfangreichen Strings

  • Allgemein

Es gibt 32 Antworten in diesem Thema. Der letzte Beitrag () ist von Bartosz.

    Vorsicht bei umfangreichen Strings

    Hallo zusammen, ich habe keine spezifische Frage, sondern wollte euch nur von meinem aktuellen Projekt berichten. Ich arbeite derzeit an einem Programm, das eine Datei eines bestimmten Formats einliest und die darin enthaltenen Werte auf der Benutzeroberfläche anzeigt.

    Um sicherzustellen, dass das Auslesen in meinem Programm fehlerfrei ist, habe ich bereits Dateien aus verschiedenen Quellen verwendet und getestet. (Es ist erstaunlich, wie große Unterschiede auftreten können, selbst wenn der Standard derselbe ist.) Vor kurzem habe ich ein YouTube-Video heruntergeladen und festgestellt, dass mein Programm über 70 Sekunden benötigte, um die Datei zu analysieren. Das war ungewöhnlich, da ähnlich große Dateien normalerweise in ein paar Dutzend Millisekunden verarbeitet wurden.

    Nach einer analytischen Untersuchung stellte sich heraus, dass die Funktion "Get_udta" besonders viel Zeit in Anspruch nimmt. Die "udta-Box", was für "user-Data" steht, enthält im Klartext Informationen wie den Autor, das Fertigungsprogramm, den Encoder-Namen und ähnliches. Dabei werden auch 639 MB Arbeitsspeicher verbraucht, obwohl die Datei selbst nur 55 MB groß ist.

    Die Ursache für diese Anomalie fand ich schließlich in der Tatsache, dass die "udta-Box" leider 820441 Bytes enthält. Diese Bytes beinhalten die gesamte Videobeschreibung des YouTube-Videos sowie (salopp gesagt) einen Großteil der Weltgeschichte. Das Programm musste über 70 Sekunden lang zusammenhängenden Speicher suchen, insbesondere weil der String zusätzlich mit
    .TrimStart(TrimChars) bearbeitet wird.

    Daher meine Bitte an euch: Seid vorsichtig mit großen Strings und programmiert eine Sicherheit ein:


    Pseudocode

    VB.NET-Quellcode

    1. If Länge > 100UI Then
    2. ' Es dauert sehr lange (mehr als 60 Sekunden), um den Speicher zu durchsuchen
    3. Me.ListOfUserData.Add("Discarded String")
    4. Continue For ' Raus aus der For-Schleife
    5. End If



    Verschoben. ~Thunderbolt
    Bilder
    • Vollbildaufzeichnung 13.11.2023 145506 2.jpg

      39,79 kB, 1.537×108, 96 mal angesehen
    • Microsoft Visual Studio 13.11.2023 15_00_22 2.jpg

      78,63 kB, 1.392×345, 83 mal angesehen

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „Thunderbolt“ ()

    ich hab post#1 noch nicht recht verstanden: Bedeutet das, dass du in einer uTube-Video-DownLoad-Datei von 55MB 880KB klartext drin enthalten war?
    Alles MetaDaten zum gezeigten Video?
    Und um aus den 55MB die 880KB Klartext auszulesen benötigt dein Proggi 70s und bläht sich auf 640MB Arbeitsspeicher auf?

    Jo, da erscheint sinnvoll, die Klartext-Lese-Funktion zu verbessern zu versuchen.
    Ob dein PseudoCode da die beste Lösung skizziert sei dahingestellt - schoma unschön ist, dass Daten dabei einfach ignoriert werden.

    Vielleicht hast du ja nur die bekannte "Todsünde" begangen, dass du denselben String in einer Schleife immer weiter verlängerst.
    Sowas würde zum genannten Fehlverhalten führen, und liesse sich auch einfach beheben - entweder mit StringBuilder oder mit List(Of String).

    Bartosz schrieb:

    Ich möchte mich an den Standard halten
    Gut, mir ist zu dem Standard nur nichts klar. Bisher konnte ich nach eigenem Gutdünken lesen.
    Eine Zeile aufschreiben sollte nicht soviel länger dauern als eine Zeile angucken, sagen wir mal pessimistisch das 3fache. Also wenn ich durch 55 MB durchrennen kann und 820kB Probleme machen ist das, etwas das mir bös missfallen würde. Ich denke wir reden über zwei verschiedene Dinge.
    Habe auch grad keine Datei zur Hand, die so groß und lesbar ist, um das zu probieren.
    @ErfinderDesRades
    ich hab post#1 noch nicht recht verstanden: Bedeutet das, dass du in einer uTube-Video-DownLoad-Datei von 55MB 880KB klartext drin enthalten war?Alles MetaDaten zum gezeigten Video?Und um aus den 55MB die 880KB Klartext auszulesen benötigt dein Proggi 70s und bläht sich auf 640MB Arbeitsspeicher auf?
    Sehr gut verstanden :)

    Vielleicht hast du ja nur die bekannte "Todsünde" begangen, dass du denselben String in einer Schleife immer weiter verlängerst. Sowas würde zum genannten Fehlverhalten führen, und liesse sich auch einfach beheben - entweder mit StringBuilder oder mit List(Of String).
    Ich nutze eine List(of String).

    @all
    Ich war die Tage stark krank, bitte entschuldigt meine verspätete Antwort.
    • Stellt euch vor, ihr habt eine Datei. Ab irgendwo darin befindet sich eine Box namens udta. Die 4 Bytes vor udta geben die Größe der Box an.
    • Die udta-Box enthält Unterboxen. Uns interessiert nur die Vorkommen von data. Die 4 Bytes vor data geben die Größe der Box an. Nochmal: Es kann mehrere 'data' geben.
    • in den data-Boxen ist jeweils ein auszulesender String enthalten. Öffnet man die Datei mit dem Notepad++, sind die klar lesbar.
    • Eine dieser data-Boxen enthält einen String mit 818110 Bytes. udta enthält als "mutterbox" 820441 Bytes.
    Aber wie schon erwähnt: Ein String mit der Länge von 818KB ist sowieso zu viel. Wer soll das lesen. Mir geht's nur darum, zu klären, warum mein System so viel Zeit benötigte, und warum es so viel Arbeitsspeicher aufbläht. Wobei letzteres, denke ich, geklärt ist. Strings sind nicht immutable. Es muss nach jeder Operation neuer Speicher gesucht werden.

    Hier ist meine Funktion:
    Spoiler anzeigen

    VB.NET-Quellcode

    1. Private Sub Get_UserData(i As Integer)
    2. Dim boxSize0 As UInt32 = Data(i - 4) * _256_3 + Data(i - 3) * _256_2 + Data(i - 2) * 256UI + Data(i - 1)
    3. For j As Integer = i + 8 To i + CInt(boxSize0) - 4 Step 1 ' nicht 4
    4. If Latin1.GetString({Data(j), Data(j + 1), Data(j + 2), Data(j + 3)}) = "data" Then
    5. Dim boxSize1 As UInt32 = Data(j - 4) * _256_3 + Data(j - 3) * _256_2 + Data(j - 2) * 256UI + Data(j - 1)
    6. If boxSize1 > 100UI Then
    7. ' It takes a very long time (more than 60 seconds) to find memory
    8. Me.ListOfUserData.Add("Discarded string")
    9. Continue For
    10. End If
    11. Dim processedData As Byte() = New Byte(CInt(boxSize1) - 8 - 1) {}
    12. Using ms As New IO.MemoryStream(Me.Data)
    13. ms.Seek(j + 4, IO.SeekOrigin.Begin)
    14. ms.Read(processedData, 0, processedData.Length)
    15. End Using
    16. ' UTF8 according to the standard
    17. Dim obtainedString As String = Text.Encoding.UTF8.GetString(processedData).TrimStart({Microsoft.VisualBasic.ControlChars.NullChar, Convert.ToChar(1), Convert.ToChar(24)})
    18. Me.ListOfUserData.Add(obtainedString)
    19. j += CInt(boxSize1) - 1 ' Do NOT write "Exit For" because these are single items
    20. End If
    21. Next
    22. End Sub


    Aufruf ist so, dass i der Index vor dem u von udta ist.
    _256_3 ist eine Konstante und steht für 256³, _256_2 für 256². ListOfUserData ist eine List(of String). Data ist mein Bytearray der Datei (einfach Data = IO.File.ReadAllBytes(fullPath))
    Ohne mich jetzt im Detail mit dem Post beschäftigt zu haben (ist mir zu wirr), warum liest du die gesamte Datei auf einmal in den Arbeitsspeicher und rennst über die Daten?
    Ein einfacher Stream wäre doch wesentlich effektiver, immerhin brauchst du nur wenige Daten aus der gesamten Datei.
    für welche Werte i wird die Methode aufgerufen? Für jedes einzelne Byte der Datei?

    Ansonsten ziemlich ineffektiv ist es, da vermutlich ziemlich oft einen MemoryStream zu bilden aus dem gesamten Data.

    auch scheint mir, dass boxSize0 enorm grosse Werte annimmt, und dementsprechend viele Umdrehungen die j-Schleife zu leisten hat.
    j wird außerdem um eine Boxgröße erhöht: ​j += CInt(boxSize1) - 1

    Ansonsten ziemlich ineffektiv ist es, da vermutlich ziemlich oft einen MemoryStream zu bilden aus dem gesamten Data.
    Dieser Stream nimmt zwar das Bytearray Data als Grundlage, liest aber nur die Größe von processedData ein, beginnend ab j+4. Deswegen verstehe ich deine Aussage nicht.

    VB.NET-Quellcode

    1. Dim processedData As Byte() = New Byte(CInt(boxSize1) - 8 - 1) {}
    2. Using ms As New IO.MemoryStream(Me.Data)
    3. ms.Seek(j + 4, IO.SeekOrigin.Begin)
    4. ms.Read(processedData, 0, processedData.Length)
    5. End Using
    na, im Stream-Konstruktor übergibst du doch Me.Data! - dadurch wird dieser Stream ein echt fettes Teil.

    wenn ich recht hingucke kannste den MemoryStreamquatsch sehr einfach ersetzen.
    Du wilslt ja einen Ausschnitt von Me.Data nach processedData kopieren.
    Dafür gibts eine Array.Copy-Überladung

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „ErfinderDesRades“ ()

    @EdR: Mit Array.Copy läuft es ein paar Sekunden schneller. Arbeitsspeichernutzung ist immer noch über 400 MB.

    VB.NET-Quellcode

    1. ​Dim processedData As Byte() = New Byte(CInt(boxSize1) - 8 - 1) {}
    2. Array.Copy(Data, j + 4, processedData, 0, processedData.Length)


    @Haudruferzappeltnoch
    Ein Stream müsste ja reichen oder?
    Nein, ich habe viele Funktionen, die kleine Portionen aus der Datei entnehmen. Das Ganze läuft objektorientiert und nach einer bestimmten Reihenfolge.

    Bartosz schrieb:

    Ok, ich denke, hier kommen wir nicht weiter. Um das Thema zu beenden: Ich werde den 820KB-String nicht in der Oberfläche anzeigen


    Ok, kein Problem. Aber es ist halt auch schwer zu helfen, wenn nicht genug relevanten Code da ist und du scheinst ja noch viel viel viel mehr im Code zu machen, dass nicht optimal läuft. Behalte nur im Hinterkopf, wenn ich jetzt als Beispiel einen 1,5MB Text in eine Textbox lade, dann steigt der Arbeitsspeicher von 6,4MB auf 16MB an. Also um ca. 10MB. Für 1,5MB Text. Wenn dein Laden in diesem speziellen Fall für diese eine Video-Datei dann noch 70 Sekunde oder was das war braucht, scheinst du ein strukturelles Problem zu haben. Das kann dir jetzt natürlich auch einfach egal sein, aber du machst vermutlich noch etwas nicht ganz richtig. :)

    Bartosz schrieb:

    Ok, ich denke, hier kommen wir nicht weiter. Um das Thema zu beenden: Ich werde den 820KB-String nicht in der Oberfläche anzeigen. :)

    Ich mache es aktuell so das ich meine Textausgaben in zwei Varianten vorliegen habe. Die lange Version geht in ein Logfile und die kurze in die Textbox. Wenn man dann noch mit StringBuilder arbeitet sollten die Zeitverluste nicht zu groß sein.
    Aktuelles Projekt: Z80 Disassembler für Schneider/Amstrad CPC :love:
    Hallo, ich habe nun neue Erkenntnisse. Ich habe mir ein Testprojekt erstellt, in dem ich eine Datei schreibe, die 818126 Bytes groß ist – bestehend aus

    Der Inhalt von data beträgt 818110 Zeichen, einfach random Zahlen von 32 bis 127 zu einem Char gemacht. Das Ganze sieht nun so aus:


    Diese Datei ist im Anhang.

    Ich habe dann ein weiteres Testprojekt erstellt, das diese Datei einliest. Ich habe dazu meine Get_UserData-Funktion aus dem Originalprojekt benutzt. Was soll ich sagen – es gibt kein Problem beim Einlesen. Der Speicherbedarf beträgt ein paar MB. Das Auslesen der Datei geht schnell.
    Das Problem ist die Übertragung an die Oberfläche. Die Listbox zeigt den langen String nicht an.

    Hier der Code zum Auslesen:
    Spoiler anzeigen

    VB.NET-Quellcode

    1. ​Public Class Form1
    2. Private Data As Byte()
    3. Private ReadOnly Latin1 As System.Text.Encoding = System.Text.Encoding.GetEncoding("iso-8859-1")
    4. Private ReadOnly ListOfUserData As New List(Of String)
    5. Private Const _256_3 As UInt32 = 16777216UI
    6. Private Const _256_2 As UInt32 = 65536UI
    7. Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
    8. Dim path As String = Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + "\Testdatei.txt"
    9. If Not System.IO.File.Exists(path) Then
    10. Return
    11. End If
    12. Me.Data = System.IO.File.ReadAllBytes(path)
    13. For i As Integer = 0 To Data.Length - 4 Step 1
    14. If $"{Convert.ToChar(Data(i))}{Convert.ToChar(Data(i + 1))}{Convert.ToChar(Data(i + 2))}{Convert.ToChar(Data(i + 3))}" = "udta" Then
    15. Get_UserData(i)
    16. End If
    17. Next
    18. ListBox1.Items.AddRange(ListOfUserData.ToArray())
    19. End Sub
    20. Private Sub Get_UserData(i As Integer)
    21. Dim boxSize0 As UInt32 = Data(i - 4) * _256_3 + Data(i - 3) * _256_2 + Data(i - 2) * 256UI + Data(i - 1)
    22. For j As Integer = i + 8 To i + CInt(boxSize0) - 8 Step 1 ' nicht 4
    23. If Latin1.GetString({Data(j), Data(j + 1), Data(j + 2), Data(j + 3)}) = "data" Then
    24. Dim boxSize1 As UInt32 = Data(j - 4) * _256_3 + Data(j - 3) * _256_2 + Data(j - 2) * 256UI + Data(j - 1)
    25. If boxSize1 > 100UI Then
    26. Me.ListOfUserData.Add("Discarded string")
    27. Continue For
    28. End If
    29. Dim processedData As Byte() = New Byte(CInt(boxSize1) - 8 - 1) {}
    30. 'Array.Copy(Data, j + 4, processedData, 0, processedData.Length)
    31. Using ms As New IO.MemoryStream(Me.Data)
    32. ms.Seek(j + 4, IO.SeekOrigin.Begin)
    33. ms.Read(processedData, 0, processedData.Length)
    34. End Using
    35. ' UTF8 according to the standard
    36. Dim obtainedString As String = System.Text.Encoding.UTF8.GetString(processedData).TrimStart({Microsoft.VisualBasic.ControlChars.NullChar, Convert.ToChar(1), Convert.ToChar(24)})
    37. Me.ListOfUserData.Add(obtainedString)
    38. j += CInt(boxSize1) - 1 ' Do NOT write "Exit For" because these are single items
    39. End If
    40. Next
    41. End Sub
    42. End Class
    Dateien
    • Testdatei.txt

      (818,13 kB, 63 mal heruntergeladen, zuletzt: )

    Bartosz schrieb:

    Die Listbox zeigt den langen String nicht an.


    Es gibt Chars die man nicht anzeigen kann. Hast du z.B. ein 0-Byte im String, siehst du nur den Teil davor. Du verwendest TrimStart, das hat nur einfluss auf Chars am Anfang des Strings.
    Zitat von mir 2023:
    Was interessiert mich Rechtschreibung? Der Compiler wird meckern wenn nötig :D