Toon posts:

[Delphi] veel objecten --> out of memory

Pagina: 1
Acties:
  • 125 views sinds 30-01-2008
  • Reageer

Verwijderd

Topicstarter
Ik loop tegen een probleem aan mbt. geheugen. De situatie:

Ik heb een NAW object, deze bevet een ID en de NAW gegevens. Grofweg gezegd ziet die er zo uit, ik heb hier de class niet geheel afgemaakt, het gaat om het idee:

Delphi:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type
  ThzNAW = class (TObject)
    private
    public
       property ID: Integer;
       property Achternaam: String;
       property Voornaam: String;
       [..] meer velden, intotaal ong 15 [..]
       function Read: Boolean;     
       constructor create(ID: Integer);
  end;

[..]

function ThzNAW.Read: Boolean
begin
  // lees velden uit database op basis van ID nummer
end;

constructor ThzNAW.Create(ID: Integer);
begin
  inherited;
  FID:=ID;
end


Omwille van database-performace, lees ik het geheel als volgt in (versimpeld weergegeven):

Delphi:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 
   oNawLijst: TStringlist.Create;
   try
     // ophalen selectie ID-nummers
     With qryHaalNAWlijst do
        begin
           Open;
           Repeat
             nNAW_ID:=FieldByName('ID').AsInteger;
             oNawLijst.AddObject(IntToStr(nNaw_ID),ThzNAW.Create(nNaw_ID));
             next; 
           Until EOF;
           Close;
        end;
     // inlezen NAW gegevens     
     For nLus:=0 to oNawLijst.Count-1 do
         ThzNAW(oNawLijst.Objects[nLus]).Read;
     [..] doe mijn shit met die lijst [..]
   finally          
       for nLus:=0 to oNawLijst.Count-1 do
          ThzNAWM(oNawLijst.Objects[nLus]).Free;
       oNawLijst.Free;
   end;


Bovenstaande code werkt perfect voor 10 NAW records, zonder memory leaks oid. Wil ik dit echter uitvoeren voor de 4400 NAW records waar ik dit voor nodig heb, dan krijg ik tijdens het inlezen van de NAW gegevens zo ongeveer halverwege de foutmelding 'OUT OF MEMORY'.

Op dat moment gebruikt mijn applicatie ca. 20Mb geheugen, iets wat acceptabel is. Mijn PC heeft dan nog zo'n 300Mb (!) fysiek geheigen vrij, dus ook geen probleem. Ik heb al de stack en heap vergroot, echter dit heeft totaal geen effect.

Wat kan ik hier nog aan doen? Het probleem is (dus) niet dat hij niet die 4400 objecten aan kan, hij kan ze alleen niet gevuld aan.

//edit
Inmiddels heb ik wel een workaround gevonden voor dit probleem: ik maakte overal gebruik van gewone strings, terwijl ik nooit strings langer heb dan 255. Vervanging van de strings door shortstrings heeft het probleem opgelost, en het geheugengebruik van de applicatie gehalveerd. Toch blijft mijn vraag staan; wat kan ik (anders) doen om dit probleem op te lossen?

[ Voor 15% gewijzigd door Verwijderd op 09-05-2003 16:58 ]


  • Kool
  • Registratie: September 1999
  • Niet online
Het kan absoluut niet dat je de melding out-of-memory krijgt bij een paar duizend simpele objectjes, dat is onmogelijk. Logisch gevolg is dus dat er iets niet klopt in je code. In de code zoals je hier zet kan ik niets fout ontdekken, maar ik ben wel benieuwd wat je b.v. in die Read functie doet.
Wat vreemd is is dat je eerst alle objecten LEEG aanmaakt en aan de lijst toevoegd, terwijl je daarna alle objecten langs loopt om ze te vullen. Dat lijkt me niet zo efficient, zowel kwa snelheid als geheugen, dat zou ik in dezelfde loop doen. Voor de rest kan ik er zoals ik deze code zie weinig over zeggen. 8)7

  • NaliXL
  • Registratie: Maart 2002
  • Laatst online: 30-07 19:19
Enneh, het maakt natuurlijk altijd uit of je een object binnen een object declareert, of dat het op de stack gedeclareerd word. Oftewel : maak eerst een basis-class als je een console-app schrijft....

Genoeg is meer dan veel, en tart den overvloed


Verwijderd

Topicstarter
Kool schreef op 09 May 2003 @ 17:18:
Het kan absoluut niet dat je de melding out-of-memory krijgt bij een paar duizend simpele objectjes, dat is onmogelijk. Logisch gevolg is dus dat er iets niet klopt in je code. In de code zoals je hier zet kan ik niets fout ontdekken, maar ik ben wel benieuwd wat je b.v. in die Read functie doet.
De code is in principe identiek aan wat hier staat, echter met meer velden. De read procedure:

Delphi:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
resourcestring
  // foutmeldingen
  cRecordNietGevonden      = 'Het record met ID %d kon niet gevonden worden in tabel %s';
  cGeenTransActie          = 'Geen transactie opgegeven voor procedure %s';
  cGeenVeldwaarde          = 'Geen veldwaarde gegeven voor veld %s in procedure %s';

const
  // gebruikte queries
  cSQLReadKandidaat : String = 'select * from NAW where id=:ID';

[..]

function ThzNAW.read: boolean;
  var
    oQuery : TIBQuery;
begin
  Result := False;
  // kijken of er wel een transactie opgegeven is
  If TransActie=Nil then Raise EGeenTransactie.CreateFmt(cGeenTransactie,['hzNAW.read']);
  // kijken of er wel een ID opgegeven is
  If FID=-1 then Raise EGeenVeldWaarde.CreateFmt(cGeenVeldwaarde,['ID','hzNAW.read']);
  // aanmaken query
  oQuery := TIBQuery.Create(nil);
  Try
    With oQuery do
      begin
        Transaction := FTransActie;
        SQL.Clear;
        SQL.Add(cSQLReadNAW);
        ParamByName('ID').AsInteger := FID;
        Open;
        // kijken of er wel een record gevonden is
        If IsEmpty then Raise ERecordNietGevonden.CreateFmt(cRecordNietGevonden,[FID,'Naw']);
        // record inlezen
        FVoornamen         := FieldByName('voornamen').AsString;
        FVoorletters       := FieldByName('voorletters').AsString;
        FAchternaam        := FieldByName('achternaam').AsString;
        FTussenvoegsel     := FieldByName('tussenvoegsel').AsString;
        FTitel             := FieldByName('titel').AsString;
        FAchtertitel       := FieldByName('achtertitel').AsString;
        FGeslacht_code     := FieldByName('geslacht_code').AsString;
        FGeboortedatum     := FieldByName('geboortedatum').AsDateTime;
        FGeboorteplaats    := FieldByName('geboorteplaats').AsString;
        FGeboorteland_code := FieldByName('geboorteland_code').AsString;
        FAdres             := FieldByName('adres').AsString;
        FPostcode          := FieldByName('postcode').AsString;
        FPlaats            := FieldByName('plaats').AsString;
        FWoonland_code     := FieldByName('woonland_code').AsString;
        Result:=True;
      end;
  Finally
    oQuery.Free;
  end;
end;
Wat vreemd is is dat je eerst alle objecten LEEG aanmaakt en aan de lijst toevoegd, terwijl je daarna alle objecten langs loopt om ze te vullen. Dat lijkt me niet zo efficient, zowel kwa snelheid als geheugen, dat zou ik in dezelfde loop doen. Voor de rest kan ik er zoals ik deze code zie weinig over zeggen. 8)7
Let op dat ik de objecten niet leeg aanmaak, ik geef ze alvast het ID mee.

De reden dat ik dit ik dit apart doet, heeft met de queries te maken. Het betreft hier een database welke niet genormaliseerdis, en welke ik niet kan/mag wijzigen. Gevolg is dat een query waarin ik in een keer alle data tevoorschijn haal, ongeveer 10 keer (!) zo lang duurt dan als ik e.e.a. splits in twee queries, vnl. omdat hij anders alle NAW gegevens meerdere keren per ID doorstuurt. Om dit te voorkomen lees ik eerst alle ID's, en haal dan met een enkelvoudige query de NAW gegevens op.

Resultaat van deze aanpak.. tijdsduur inlezen voor: ong 30 minuten (!), tijdsduur inlezen na: 2 minuten. Zet ik het inlezen op de plek waar het object aangemaakt wordt, dan nog doet hij er zo'n 15 minuten over. Ik maak gebruik van Interbase en de IBExpres objecten.

[ Voor 12% gewijzigd door Verwijderd op 09-05-2003 17:43 ]


Verwijderd

Topicstarter
NaliXL schreef op 09 May 2003 @ 17:26:
Enneh, het maakt natuurlijk altijd uit of je een object binnen een object declareert, of dat het op de stack gedeclareerd word. Oftewel : maak eerst een basis-class als je een console-app schrijft....
Deze mag je uitleggen. De door mij gekozen aanpak is imo vrij standaard, dat is juist waar de hele functie addobject van de class tstringlist voor bedoeld is.

Wat is je voorstel? Een container-klasse definieren voor die lijst? Dat komt imo op exact hetzelfde neer, de klasse tStringlist is niets meer en minder dan een container-klasse, ik zie niet in waarom ik die nog eens opnieuw zou moeten gaan zitten schrijven.

Overigens bewijst het feit dat de versie met shortstrings wel werkt (en zonder memory leaks volgens de bekende testsoftware), dat er niet zozeer een 'fout' in de code zit, maar eerder dat het een compiler-aangelegenheid is, of dat ik ergens specifiek geheugen moet gaan zitten alloceren. De vraag is alleen waar en hoe?

//edit
Ik denk dat er een bug zit in de code voor strings, in Delphi 6. Als ik namelijk de shortstring gebruik, is er geen probleem. Tijdens het eigenlijke inlezen van de gegevens zie ik geen noemenswaardige toename in gebruikt geheugen, hooguit 2 Mb voor 1000 NAW records. Vervang ik de shortstrings voor string dan komt hij halverwege de 1000 al met die outofmemory error, en is het geheugengebruik met ca. 20Mb toegenomen.

Om dit beter te testen, heb ik 'm voor de gein eens 40.000 NAW gegevens laten inlezen gebruik makende van shortstrings. Dat doet hij zonder problemen, geen foutmeldingen en geen memleaks.

Mijn conclusie: iets in de definitie van de ansistring zorgt er voor dat hij geheugen niet goed alloceert.

[ Voor 27% gewijzigd door Verwijderd op 09-05-2003 20:17 ]


  • LordLarry
  • Registratie: Juli 2001
  • Niet online

LordLarry

Aut disce aut discede

Het probleem is toch de TStringList waar je je objecten in stopt. De stringlist weet niet hoe groot ie gaat worden en reserveerd geheugen met een aantal tegelijk ipv elke keer 1 om op deze manier een beetje sneller te zijn. Maar elke keer dat de stringlist groeit realloceerd ie het geheugen. Dat betekend dat een nieuw stuk geheugen gezocht wordt die groot genoeg is, maar veroorzaakt wel veel fragmentation in het geheugen. Dus niet alleen is het vrij langzaam, het zorgt er ook nog voor dat je je geheugen niet optimaal kan benutten.

Zet TStringList.Capacity zo dicht mogelijk in de buurt van het aantal items je denkt in te lezen. Of schrijf je eigen betere list classe (TList heeft hetzelfde effect, maar reserveert minder (alleen pointer)). Of probeer een andere memory manager (http://www.torry.net/debug.htm, http://www.optimalcode.com/memmgr.htm)

We adore chaos because we like to restore order - M.C. Escher


  • LordLarry
  • Registratie: Juli 2001
  • Niet online

LordLarry

Aut disce aut discede

Verwijderd schreef op 09 May 2003 @ 17:40:
Mijn conclusie: iets in de definitie van de ansistring zorgt er voor dat hij geheugen niet goed alloceert.
Wel goed, alleen anders. Heap vs Stack

We adore chaos because we like to restore order - M.C. Escher


Verwijderd

Topicstarter
Het probleem is toch de TStringList waar je je objecten in stopt. De stringlist weet niet hoe groot ie gaat worden en reserveerd geheugen met een aantal tegelijk ipv elke keer 1 om op deze manier een beetje sneller te zijn. Maar elke keer dat de stringlist groeit realloceerd ie het geheugen. Dat betekend dat een nieuw stuk geheugen gezocht wordt die groot genoeg is, maar veroorzaakt wel veel fragmentation in het geheugen. Dus niet alleen is het vrij langzaam, het zorgt er ook nog voor dat je je geheugen niet optimaal kan benutten.
Hoe verklaart dit dan dat het wel werkt met shortstrings?

De objecten zelf heeft hij ook geen problemen mee, het is pas als hij de strings van die objecten gaat vullen als hij met een foutmelding komt.

  • LordLarry
  • Registratie: Juli 2001
  • Niet online

LordLarry

Aut disce aut discede

Omdat dat het punt is dat AnsiStrings pas geheugen gaan reserveren voor de inhoud. Er zitten enorm grote verschillen tussen een ShortString en een LongString. Maar het zal waarschijnlijk een combinatie zijn van wat jij beschrijft en wat ik beschrijf.

We adore chaos because we like to restore order - M.C. Escher


Verwijderd

Topicstarter
Dat begrijp ik, ik spreek je ook niet tegen.. ik wil alleen maar weten waar dit precies vandaan komt. De reden dat ik aan een bug denk, is het feit dat hij zoveel geheugen gaat gebruiken.

Ik kan me voorstellen dat de TStringlist geheugen gaat reserveren op basis van het object zoals hij 'm op dat ogenblik aangeboden krijgt. Als ik dan later (zeg maar, zonder dat de tstringlist daar weet van heeft) dat object zo ga aanpassen dat hij meer geheugen nodig heeft (de strings alloceren idd. zelf geheugen), dan kan ik me voorstellen dat dat tot een fout leid. Tot zover klopt het allemaal nog wel.

Wat imo echter niet klopt, is het geheugengebruik. Waarom zie ik bij shortstrings niet zo'n exessief geheugengebruik, en bij normale strings (=ansistrings) wel?

Ik heb er nu geen tijd voor/zin in, maar het kan interessant zijn dit in de VCL op te zoeken.

Verwijderd

Verwijderd schreef op 09 May 2003 @ 16:38:
Ik loop tegen een probleem aan mbt. geheugen. De situatie:

...

Bovenstaande code werkt perfect voor 10 NAW records, zonder memory leaks oid. Wil ik dit echter uitvoeren voor de 4400 NAW records waar ik dit voor nodig heb, dan krijg ik tijdens het inlezen van de NAW gegevens zo ongeveer halverwege de foutmelding 'OUT OF MEMORY'.
Wellicht heb je hier wat aan?
http://bdn.borland.com/article/0,1410,27659,00.html

Check de UniDirectional property.


Probeer ook eens de Capacity property van de TStringList van te voren op een hele hoge waarde te zetten, want als deze te klein is en je voegt dan een nieuwe regel toe, dan wordt er nieuw aaneengesloten geheugen gealloceerd met blokken van x. En daar wordt dan alles naar toe gekopieerd.


Welke versie van Delphi gebruik je?


http://www.optimalcode.com/memmgr.htm

[ Voor 20% gewijzigd door Verwijderd op 11-05-2003 23:09 ]


  • Tomatoman
  • Registratie: November 2000
  • Laatst online: 01:25

Tomatoman

Fulltime prutser

Wat dacht je van een paar optimalisaties die onnodige geheugenallocaties tegengaan? Creëer niet 4000 keer dezelfde query te creëren, maar hergebruik deze. Dat scheelt je 4000 keer het creëren van een TIBQuery, 4000 keer een impliciete Prepare en UnPrepare en je geeft de Interbase Server de kans om veel efficiënter met de query om te gaan. De enige voorwaarde is dat je niet meer een aparte transactie per ThzNAW object gebruikt, maar één transactie voor de enige query die je overhoudt. Ik durf te wedden dat je code véél sneller wordt en minder kans heeft op vage Out-of-Memory-foutmeldingen. (Misschien kreeg je wel een Out of Memory omdat je teveel gelijktijdige transacties had.)

Delphi:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
type
  ThzNAW = class (TObject)
  private
    FID: Integer;
    procedure SetID(Value: Integer);
  public
    property ID: Integer read FID write SetID;
    ...
    function Read(IBQuery: TIBQuery): Boolean;     
    constructor Create(AID: Integer);
  end;

procedure ThzNAW.SetID(Value: Integer);
begin
  if Value = -1 then
    raise EGeenVeldWaarde.CreateFmt(cGeenVeldwaarde, ['ID',
      'hzNAW.SetID']);
  FID := Value;
end;

constructor ThzNAW.Create(AID: Integer);
begin
  inherited;
  ID := AID;
end;

function ThzNAW.Read(IBQuery: TIBQuery): Boolean;
begin
  Result := False;
  with IBQuery do
  begin
    Transaction := FTransactie;
    ParamByName('ID').AsInteger := FID;
    if Active then
      Refresh
    else
      Open;
    // kijken of er wel een record gevonden is
    if IsEmpty then
      raise ERecordNietGevonden.CreateFmt(cRecordNietGevonden,
        [FID, 'NAW']);
    // record inlezen
    FVoornamen         := FieldByName('voornamen').AsString;
    { en de rest van de velden }

    Result := True;
  end;
end;
Delphi:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
var
  IBQuery: TIBQuery;
begin
  oNawLijst: TStringlist.Create;
  try
    oNawLijst.Capacity := 1024;  // of een andere waarde
    // ophalen selectie ID-nummers
    with qryHaalNAWlijst do
    try
      Open;
      repeat
        nNAW_ID := FieldByName('ID').AsInteger;
        oNawLijst.AddObject(IntToStr(nNaw_ID), ThzNAW.Create(nNaw_ID));
        Next;
      until EOF;
    finally
      Close;
    end;

    IBQuery := TIBQuery.Create(nil);
    try
      IBQuery.SQL.Text := cSQLReadNAW;
      IBQuery.Prepare;

      // start een database transactie, bijvoorbeeld:
      IBQuery.Database.DefaultTransaction.StartTransaction;

      // inlezen NAW gegevens
      for nLus := 0 to oNawLijst.Count -1 do
      try
        ThzNAW(oNawLijst.Objects[nLus]).Read(IBQuery);
      except
        on ERecordNietGevonden do
          { reageer hierop en ga door met de lus }
        else
          raise;
      end;

      { doe jouw shit met die lijst }

    finally
      // beeindig de database transactie, bijvoorbeeld:
      IBQuery.Database.DefaultTransaction.Rollback;

      IBQuery.Close;
      IBQuery.UnPrepare;
      IBQuery.Free;
    end;
  finally
    for nLus := oNawLijst.Count -1 downto 0 do
      ThzNAWM(oNawLijst.Objects[nLus]).Free;
    oNawLijst.Free;
  end;
end;

Tip: je tabstops staan nogal raar ingesteld.

[ Voor 37% gewijzigd door Tomatoman op 11-05-2003 23:43 . Reden: diverse verbeteringen ]

Een goede grap mag vrienden kosten.


Verwijderd

Topicstarter
Verwijderd schreef op 11 May 2003 @ 23:05:
[...]
Wellicht heb je hier wat aan?
http://bdn.borland.com/article/0,1410,27659,00.html

Check de UniDirectional property.
Dat is zeker een goede tip, ondanks het feit dat ie een beetje off-topic is. Feit is dat ik veel data sequentieel inlees, dus idd. geen bi-directional datasets nodig heb (=langzamer). maw: ja ik heb er wat aan :)
Probeer ook eens de Capacity property van de TStringList van te voren op een hele hoge waarde te zetten, want als deze te klein is en je voegt dan een nieuwe regel toe, dan wordt er nieuw aaneengesloten geheugen gealloceerd met blokken van x. En daar wordt dan alles naar toe gekopieerd.
Is een optimalisatie welke ik dankzij de reacties in deze thread al doorgevoerd heb. Het zou voor de werking echter niet nodig moeten zijn (uit de Delphi help):
Adding new strings will cause the Capacity property to increase if necessary
Welke versie van Delphi gebruik je?
Delphi 6 Professional
tomatoman schreef op 11 mei 2003 @ 23:08:
Wat dacht je van een paar optimalisaties die onnodige geheugenallocaties tegengaan?
Lijkt me een goed plan :)
Creëer niet 4000 keer dezelfde query te creëren, maar hergebruik deze. Dat scheelt je 4000 keer het creëren van een TIBQuery, 4000 keer een impliciete Prepare en UnPrepare en je geeft de Interbase Server de kans om veel efficiënter met de query om te gaan.
Geheel waar. Gelijk gedaan...
De enige voorwaarde is dat je niet meer een aparte transactie per ThzNAW object gebruikt, maar één transactie voor de enige query die je overhoudt.
(Misschien kreeg je wel een Out of Memory omdat je teveel gelijktijdige transacties had.)
Ik gebruikte al 1 transactie. Elk object heeft wel z'n eigen transactie property, maar dat zijn gewoon allemaal pointers naar 1 transactie. Maar goed, wel prettig om die property uit m'n kandidaat class te hebben, deze oplossing is sowieso mooier.. bedankt voor de input :)

Ondanks de optimalisaties (waarvoor dank!) blijft overigens de out of memory error bij gebruik van ansistrings gewoon bestaan. Omwille van tijdsdruk, alsmede het geheugengebruik van de ansistrings, en als laatste het feit dat shortstrings toereikend zijn, laat ik het even voor wat het is. Op de TODO lijst staat wel het maken van een eigen container-class, wat het geheugenprobleem met ansistrings zou moeten oplossen imo.

Als iemand antwoord weet op de vraag waarom het gebruik van ansi strings zoveel meer geheugen kost, dan hoor ik dat graag. Als iemand dat in dit topic al aangegeven heeft: rephrase please :)

  • LordLarry
  • Registratie: Juli 2001
  • Niet online

LordLarry

Aut disce aut discede

Verwijderd schreef op 12 mei 2003 @ 00:16:
Is een optimalisatie welke ik dankzij de reacties in deze thread al doorgevoerd heb. Het zou voor de werking echter niet nodig moeten zijn (uit de Delphi help):
Adding new strings will cause the Capacity property to increase if necessary
Klopt, maar zoals ik al zei kost dat wel veel tijd en veroorzaakt fragmentatie. Elke keer dat de Capacity vergroot wordt, wordt een nieuw stuk geheugen gereserveerd en de oude gegevens erin gekopieeerd waarna het oude stuk geheugen vrijgegeven wordt.
Als iemand antwoord weet op de vraag waarom het gebruik van ansi strings zoveel meer geheugen kost, dan hoor ik dat graag. Als iemand dat in dit topic al aangegeven heeft: rephrase please :)
Ik ben nu ook wel benieuwd en als ik wat tijd heb zoek ik het voor je uit.

We adore chaos because we like to restore order - M.C. Escher

Pagina: 1