Delphi-PRAXiS
Seite 1 von 2  1 2      

Delphi-PRAXiS (https://www.delphipraxis.net/forum.php)
-   Object-Pascal / Delphi-Language (https://www.delphipraxis.net/32-object-pascal-delphi-language/)
-   -   Delphi Memory-Leck? Verkettete Records brauchen extrem viel Ram... (https://www.delphipraxis.net/44169-memory-leck-verkettete-records-brauchen-extrem-viel-ram.html)

Eskayp 14. Apr 2005 17:16


Memory-Leck? Verkettete Records brauchen extrem viel Ram...
 
Hallo Leute,

ich würde mich nicht als Delphi-Noob bezeichnen, aber auch noch lange nicht als Profi.

Eine von mir programmierte Unit soll ein verschlüsseltes Datenbankfile einlesen und daraus Daten filtern und zurückgeben können. Das ganze soll später mal in eine DLL verpackt werden, daher arbeite ich mit ShortStrings.

Das Datenbankfile hat verschlüsselt (nicht komprimiert) gerade mal 260 KB. Wenn ich dies in meiner Anwendung nur einlese und in den verketteten Records speichere, braucht meine Anwendung erstens mehrere Sekunden und belegt dann zweitens 21 MB im Ram (ohne geladene Daten nur 2.5 MB).

Also irgendwo kann dann da doch was nicht stimmen, oder? Bin für jede Hilfe dankbar.

Ach, fragt bitte nicht, warum ich nicht ne fertige DB-Schnittstelle nutze :) hat so seine Gründe...

Der Übersichtlichkeit halber nur Codeausschnitte...

Delphi-Quellcode:
unit CodeTestDLL;

interface

uses
  SysUtils,
  Dialogs,
  Classes;

type
  PTmyDBRow = ^TmyDBRow;
  TmyDBRow = record
    value: Array of ShortString;
    marker: Boolean;
    next: PTmyDBRow;
  end;

  PTmyDBField = ^TmyDBField;
  TmyDBField = record
    name: ShortString;
    next: PTmyDBField;
  end;

  PTmyDB = ^TmyDB;
  TmyDB = record
    name: ShortString;
    fieldcnt: Integer;
    fields: PTmyDBField;
    data: PTmyDBRow;
    next: PTmyDB;
  end;

  PTmyTmp = ^TmyTmp;
  TmyTmp = record
    value: ShortString;
    next: PTmyTmp;
  end;

  procedure MyDBDecode(var pLine: ShortString);
  procedure MyDBExplode(pHaystack: String);
  function MyDBFieldNr(FieldName: ShortString; var p: Integer): Boolean;
  function MyDBGetError(): ShortString; stdcall;
  function MyDBLoad(DBName, FileName: ShortString): Boolean;stdcall;
  procedure MyDBSelect(DBName, DBFields: ShortString); stdcall;
  procedure MyDBFilter(FieldName, Value: ShortString); stdcall;
  function MyDBGetResult(var Values: ShortString): Boolean; stdcall;


var
  myDBs: PTmyDB;
  myLastDB: ShortString;
  myLastFields: ShortString;
  myResult: PTmyDBRow;
  myTmpVal: PTmyTmp;
  myError: ShortString;

  curDB: PTmyDB;

const
  mySepStr       : Char       = '|';
  myErrOpenFailed : ShortString = 'Öffnen der Datenbank fehlgeschlagen';
  myErrWrongHeader: ShortString = 'Format der Datenbank unbekannt';
  myErrCorrupt   : ShortString = 'Datenbank korrupt';
  myFileHeader   : ShortString = 'waspdbfile_v1.0';

implementation

// Da die Datenbankdateien verschlüssel sind, muss jede einzelne Zeile mit Hilfe
// dieser Routine entschlüsselt werden. Ist nicht gerade extrem sicher, aber
// reicht gegen "normale Hacker" ;)
procedure MyDBDecode(var pLine: ShortString);
  // [...]
end;

// Das Aufsplitten einer Zeile anhand des SepStrings übernimmt diese Routine.
// Das Ergebnis wird in einem verketteten, temporärem Record gespeichert. So
// kann es dann später leicht in verschiedene Datenformen umgewandelt werden.
procedure MyDBExplode(pHaystack: String);
var
  i: Integer;
  sValue: String;
  TmpVal: PTmyTmp;
begin
  TmpVal := nil;

  // Erstmal das temporäre Feld löschen, falls noch voll
  while (myTmpVal <> nil) do begin
    TmpVal := myTmpVal;
    myTmpVal := myTmpVal^.next;
    Dispose(TmpVal);
  end;

  // Dann Stück für Stück die Zeile durchgehen und bei Auffinden des SplitChr
  // den so gesammelten Wert in das verkettete Record setzen.
  sValue := '';
  for i := 1 to Length(pHaystack) do begin
    if (pHaystack[i] = mySepStr) then begin
      // Zwischenwert im verketteten Record speichern
      if (myTmpVal = nil) then begin
        New(myTmpVal);
        TmpVal := myTmpVal;
      end
      else begin
        New(TmpVal^.next);
        TmpVal := TmpVal^.next;
      end;
      TmpVal^.value := sValue;
      TmpVal^.next := nil;

      sValue := ''; // Zwischenspeicher zurücksetzen
    end
    else begin
      sValue := sValue + pHaystack[i];
    end;
  end;
end;

// Jedes Feld bekommt ja eine Nummer. Diese Routine ermittelt die zugehörige
// Nummer eines Feldes (=Spalte in Tabelle).
function MyDBFieldNr(FieldName: ShortString; var p: Integer): Boolean;
var
  curField: PTmyDBField;
begin
  // An welcher Stelle steht denn Fieldname?
  curField := curDB^.fields;
  p := 0;
  while (curField <> nil) and (curField^.name <> FieldName) do begin
    curField := curField^.next;
    Inc(p);
  end;
  Result := (curField <> nil);
end;

// Diese Routine soll zu beginn der Anwendung erst einmal alle verfügbaren
// Datenbankdateien einlesen und in mehreren verketteten Records speichern.
// Die Routine sucht die Datenbanken nicht selber, sondern bekommt jede einzeln
// per Parameter (wird also pro DB einmal aufgerufen).
function MyDBLoad(DBName, FileName: ShortString): Boolean; stdcall;
var
  DBFile: TextFile;
  FileOpened: Boolean;
  Line: ShortString;
  TmpVal: PTmyTmp;
  curField: PTmyDBField;
  curRow: PTmyDBRow;
  cnt, n: Integer;
begin
  myError := '';
  Result := false;
  FileOpened := false;

  curField := nil;
  curRow := nil;
  cnt := 0;

  // Datei öffnen
  AssignFile(DBFile, FileName);
  {$I-}
  FileMode := 0;
  Reset(DBFile);

  // Dateiheader lesen
  if (IOResult = 0) then begin
    FileOpened := True;
    Readln(DBFile, Line);
  end
  else myError := myErrOpenFailed;

  if (myError = '') then begin
    if (IOResult <> 0) then myError := myErrOpenFailed;
  end;

  // Dateispalten einlesen ...
  if (myError = '') then begin
    if (Line = myFileHeader) then begin
      Readln(DBFile, Line);
    end
    else myError := myErrWrongHeader;
  end;

  // ... und merken
  if (myError = '') then begin
    if (IOResult = 0) then begin
      // Jetzt kommen ja definitiv Daten, also muss die neue Datenbank
      // angelegt werden
      if (myDBs = nil) then begin
        New(myDBs);
        curDB := myDBs;
      end
      else begin
        curDB := myDBs;
        while (curDB^.next <> nil) do
          curDB := curDB^.next;
        New(curDB^.next);
        curDB := curDB^.next;
      end;
      curDB^.name := DBName;
      curDB^.fields := nil;
      curDB^.data := nil;
      curDB^.next := nil;

      // Daten entschlüsseln und splitten
      MyDBDecode(Line);
      MyDBExplode(Line);

      // Daten im Field-Record speichern und Felder (=Spalten) zählen
      cnt := 0;
      TmpVal := myTmpVal;
      while (TmpVal <> nil) do begin
        if (curDB^.fields = nil) then begin
          New(curDB^.fields);
          curField := curDB^.fields;
        end
        else begin
          New(curField^.next);
          curField := curField^.next;
        end;
        curField^.name := TmpVal^.value;
        curField^.next := nil;

        TmpVal := TmpVal^.next;
        Inc(cnt);
      end;

      curDB^.fieldcnt := cnt;
    end
    else myError := myErrCorrupt;
  end;

  // Daten einlesen und im Array ablegen
  if (myError = '') then begin
    while not Eof(DBFile) do begin
      Readln(DBFile, Line);
      if (IOResult = 0) and (Line <> '') then begin
        // Daten entschlüsseln und splitten
        MyDBDecode(Line);
        MyDBExplode(Line);
        // Neue Zeile (Datensatz, Row) erzeugen
        if (curDB^.data = nil) then begin
          New(curDB^.data);
          curRow := curDB^.data;
        end
        else begin
          New(curRow^.next);
          curRow := curRow^.next;
        end;
        SetLength(curRow^.value, cnt);
        curRow^.marker := true;
        curRow^.next := nil;

        // Werte in der Zeile speichern
        TmpVal := myTmpVal;
        n := 0;
        while (TmpVal <> nil) do begin
          if (n < cnt) then begin
            curRow^.value[n] := TmpVal^.value;
            TmpVal := TmpVal^.next;
          end;
          Inc(n);
        end;
      end;
    end;
    Result := true;
  end;

  {$I+}

  // Datei schließen
  if (FileOpened = true) then begin
    CloseFile(DBFile);
  end;
end;

// [...]
Hui, ist doch noch viel Code übrig geblieben... hoffe Ihr steigt durch!

Vielen Dank schonmal im Voraus für Eure Hilfe!

kiar 14. Apr 2005 17:23

Re: Memory-Leck? Verkettete Records brauchen extrem viel Ram
 
hallo,

willkommen in der dp :mrgreen:

hast du schon mal deine Anwendung mit memproof gestartet?

wäre erstmal ein erster Versuch.

raik

Basilikum 14. Apr 2005 17:29

Re: Memory-Leck? Verkettete Records brauchen extrem viel Ram
 
ein Grund könnten auch die vielen ShortStrings sein, die - egal ob sie '' oder einen riesen String enthalten - pauschal 256 Byte belegen.... ev. lässt sich der Speicherverbrauch bereits mit String[x] reduzieren, wenn man weiss, das gewisse Felder nie mehr als x Zeichen enthalten werden (String[X] ist genau wie ShortStrings problemlos in/mit DLLs).

jbg 14. Apr 2005 17:32

Re: Memory-Leck? Verkettete Records brauchen extrem viel Ram
 
Zitat:

Das ganze soll später mal in eine DLL verpackt werden, daher arbeite ich mit ShortStrings.
Und warum benutzt du dann dynamische Arrays?

Zitat:

sValue := sValue + pHaystack[i];
Damit hebelst du schon mal die Speichermanagerlogik aus und das Programm fängt an virtuellen Speicher zwischenzuspeichern. (das ist aber kein Speicherleck und wird somit von MemProof nicht angezeigt. Den Grund für das Verhalten kannst du hier im Forum mit der Suchfunktion finden).

Bernhard Geyer 14. Apr 2005 18:53

Re: Memory-Leck? Verkettete Records brauchen extrem viel Ram
 
Zitat:

Zitat von Eskayp
Eine von mir programmierte Unit soll ein verschlüsseltes Datenbankfile einlesen und daraus Daten filtern und zurückgeben können.
...
Ach, fragt bitte nicht, warum ich nicht ne fertige DB-Schnittstelle nutze Smile hat so seine Gründe...

Für verschlüsselte Datenbankfiles könntest Du auch gleiche eine fertige Datenbank nehmen (Evtl. sprechen deine Geheimen Gründe dagegen). ADS Local Server unterstützt das verschlüsseln seiner Datenbankdateien.

Zitat:

Zitat von Eskayp
Das Datenbankfile hat verschlüsselt (nicht komprimiert) gerade mal 260 KB. Wenn ich dies in meiner Anwendung nur einlese und in den verketteten Records speichere, braucht meine Anwendung erstens mehrere Sekunden und belegt dann zweitens 21 MB im Ram (ohne geladene Daten nur 2.5 MB).

Zur überprüfung von Speicherlöchern könntest Du auch MemCheck verwenden.

Speedmaster 14. Apr 2005 18:55

Re: Memory-Leck? Verkettete Records brauchen extrem viel Ram
 
Willkommen in der DP², und eins muss man dir gleich lassen: Super Kommentiert der Code!

Eskayp 15. Apr 2005 11:22

Re: Memory-Leck? Verkettete Records brauchen extrem viel Ram
 
Wow, das geht ja schnell hier :)

Hmm, also Memproof scheint anzuzeigen (wenn ich das Tool richtig verstehe), dass es halt viele viele reservierte Speicherbereiche gibt, die alle die gleiche Größe haben. Das werden dann also die einzelnen Zeilen als Records sein.

Jetzt stellt sich nur die Frage: wie kann ich denn nun die Größe reduzieren? Die Strings sind natürlich unterschiedlich lang. Es handelt sich bei einer Tabelle z.B. um eine Fahrzeugtabelle. Da gibt es dann Fahrzeuge der Marke "BMW" und Fahrzeuge der Marke "Mitsubishi". Insgesamt ist das längste Feld 25 Zeichen lang, das kürzeste 0. Mit Strings einer festen Länge zu arbeiten ist also auch nicht sinnvoll.

Bringt es eigentlich was, packed records daraus zu machen? Muss ich mal ausprobieren.

---

Neben der Verschlüsselung gibt es noch andere Gründe, das Datenhandling selber zu übernehmen:
1. Da es sich um eine Auftragsarbeit handelt und das fertige Programm auf CD verteilt werden soll, muss ich genauestens auf Lizenzen achten.
2. Da sich das Datenformat ändern könnte (ist im Moment noch nicht abzusehen) müsste die Datenbankroutine ggf. ausgetauscht werden. Daher wird diese auch später in eine DLL ausgelagert.
3. Die selbe DLL wird dann wahrscheinlich auch für andere Projekte genutzt werden. Und da ist es schon besser, wenn man weiss, was da genau drin steckt :)

---

@Speedmaster: Vielen Dank für das Lob :) da ich normalerweise überwiegend in PHP programmiere, muss ich mir selber immer sagen, was ich denn da getan habe, hehe...

---

@jpg: Den dyn. Arrays liegt folgende Überlegung zugrunde: ich möchte mit ja im Prinzip mit Hilfe meiner Records ein zweidimensionales System mit ein paar Zusatzdaten abbilden. Wenn ich jetzt nach einem Datensatz des Herstellers "Mercedes" suche und die dazugehörigen Fahrzeuge ausgeben möchte, muss ich ja erstmal gucken, welche Spalte den Titel "Hersteller" trägt, und welche den Titel "Modell". Dann müsste ich in jeder Zeile die verk. Records von vorne nach hinten durchlaufen. Das wollte ich mir mit den dyn. Arrays sparen. Man hätte dann allerdings auch gleich ein kompl. 2-dim. Array machen können. Hmm. So hab ich ne Mischung gemacht, die jetzt im Nachhinein tatsächlich völlig bekloppt ist *g*
Ich änder das!

Aber wie mach ich das "sValue := sValue + pHaystack[i];" denn besser?

jim_raynor 15. Apr 2005 11:28

Re: Memory-Leck? Verkettete Records brauchen extrem viel Ram
 
Zitat:

Zitat von Eskayp
Aber wie mach ich das "sValue := sValue + pHaystack[i];" denn besser?

Such mal nach Explode in der Codelib. Das soll doch die Funktion machen oder?

Christian Seehase 15. Apr 2005 12:16

Re: Memory-Leck? Verkettete Records brauchen extrem viel Ram
 
Moin Eskayp,

packed records werden Dir vor allem einmal einen Geschwindigkeitsverlust bringen ;-)

Wie viele Sätze/Felder hat denn die Datei?

Eskayp 15. Apr 2005 12:43

Re: Memory-Leck? Verkettete Records brauchen extrem viel Ram
 
Ok, die Explode-Routine hab ich neu geschrieben und ein wenig optimiert.
Jetzt müssen nicht mehr so viele New- und Dispose-Befehle ausgeführt werden.

Delphi-Quellcode:
// Das Aufsplitten einer Zeile anhand des SepStrings übernimmt diese Routine.
// Das Ergebnis wird in einem verketteten, temporärem Record gespeichert. So
// kann es dann später leicht in verschiedene Datenformen umgewandelt werden.
procedure MyDBExplode(pHaystack: String);
var
  i, n: Integer;
  TmpVal, NextTmpVal: PTmyTmp;
begin
  // Optimierung:
  // Da eigentlich alle Felder gleich lang sind, muss myTmpVal nicht jedesmal
  // gelöscht werden, sondern kann wiederverwendet werden. Evntl. muss dafür
  // das Feld aber dann eben mittendrin erweitert werden, oder eben am Ende
  // gestutzt.
  TmpVal := myTmpVal;

  n := 0;

  // Jetzt nach den Trennzeichen suchen und Ergebnisse im verk. Record
  // nyTmpVal speichern.
  i := Pos(mySepStr, pHaystack);
  while (i > 0) do begin
    // Diesen gefundenen Wert jetzt im verk. Record speichern
    if (myTmpVal = nil) then begin
      New(myTmpVal);
      TmpVal := myTmpVal;
      TmpVal^.next := nil;
    end
    else if (n = 0) then begin
      // nichts tun
    end
    else if (TmpVal^.next = nil) then begin
      New(TmpVal^.next);
      TmpVal := TmpVal^.next;
      TmpVal^.next := nil;
    end
    else begin
      TmpVal := TmpVal^.next;
    end;
    TmpVal^.value := Copy(pHaystack, 1, i - 1);

    // Haystack um das gefundene reduzieren und weiter suchen
    pHaystack := Copy(pHaystack, i + 1, Length(pHaystack) - i);
    i := Pos(mySepStr, pHaystack);
    Inc(n);
  end;
  // Letzten gefundenen Wert natürlich auch noch hinzufügen
  if (pHaystack <> '') then begin
    if (myTmpVal = nil) then begin
      New(myTmpVal);
      TmpVal := myTmpVal;
      TmpVal^.next := nil;
    end
    else if (n = 0) then begin
      // nichts tun
    end
    else if (TmpVal^.next = nil) then begin
      New(TmpVal^.next);
      TmpVal := TmpVal^.next;
      TmpVal^.next := nil;
    end
    else begin
      TmpVal := TmpVal^.next;
    end;
    TmpVal^.value := pHaystack;
  end;

  // Wenn es jetzt nach dem letzten gesp. Wert noch weitere Felder gibt
  // müssen diese gelöscht werden.
  if (TmpVal <> nil) then begin
    NextTmpVal := TmpVal^.next;
    TmpVal^.next := nil;
    TmpVal := NextTmpVal;
    while (TmpVal <> nil) do begin
      NextTmpVal := TmpVal^.next;
      Dispose(TmpVal);
      TmpVal := NextTmpVal;
    end;
  end;
end;
Außerdem habe ich einen ziemlich gravierenden Fehler gefunden:
Delphi-Quellcode:
  // Werte in der Zeile speichern
  TmpVal := myTmpVal;
  n := 0;
  while (TmpVal <> nil) do begin
    if (n < cnt) then begin
      curRow^.value[n] := TmpVal^.value;
      TmpVal := TmpVal^.next;
    end;
    Inc(n);
  end;
muss natürlich lauten:
Delphi-Quellcode:
  // Werte in der Zeile speichern
  TmpVal := myTmpVal;
  n := 0;
  while (TmpVal <> nil) do begin
    if (n < cnt) then
      curRow^.value[n] := TmpVal^.value;
    TmpVal := TmpVal^.next;
    Inc(n);
  end;
bzw. noch besser:
Delphi-Quellcode:
  // Werte in der Zeile speichern
  TmpVal := myTmpVal;
  n := 0;
  while ((TmpVal <> nil) and (n < cnt)) do begin
    curRow^.value[n] := TmpVal^.value;
    TmpVal := TmpVal^.next;
    Inc(n);
  end;
Die Einführung von packed records hat so gut wie gar nichts gebracht. Dafür aber die Reduzierung von Array of Shortstring auf Array of String[25]. Und zwar ganz gewaltig! Vielen Dank schonmal an Basilikum für diesen Hinweis. Jetzt muss ich noch das dyn. Array wegrationalisieren ;)


Alle Zeitangaben in WEZ +1. Es ist jetzt 05:35 Uhr.
Seite 1 von 2  1 2      

Powered by vBulletin® Copyright ©2000 - 2024, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024 by Thomas Breitkreuz