Entwickler-Ecke

Delphi Language (Object-Pascal) / CLX - TStringList.AddObject -> EOufOfMemory


AScomp - Mi 09.01.13 00:04
Titel: TStringList.AddObject -> EOufOfMemory
Hallo,

zum Schreiben eines Logs nutze ich derzeit eine TStringList, die per AddObject jeweils einen Dateinamen sowie den zugehörigen Zeitstempel zugewiesen bekommt. Da es sehr umfangreiche Operationen sind, kann das Log durchaus mehrere hundertausend Einträge enthalten - was früher oder später zu einem EOutOfMemory führt. Ich vermute, das liegt am Speichermanager von Delphi XE (zumindest glaube ich mich zu erinnern, dass Delphi mit SetLength schon früher Probleme hatte, was vermutlich bei AddObject intern verwendet wird, um einen neuen/größeren zusammenhängenden Speicherbereich zu erhalten).

Jetzt wäre also die Frage, wie ich dieses Problem in den Griff bekommen kann. Da im Voraus nicht klar ist, wie viele Einträge es im Log geben wird, kann ich die Größe schlecht vorher bestimmen. Natürlich könnte ich einfach auf Verdacht mal 10.000 Einträge hinzufügen und sobald der 10.001. kommt, weitere 10.000 hinzufügen usw. Aber ich hoffe, dass es eine elegantere Möglichkeit gibt.

Hat mir jemand einen Tipp?

Vielen Dank!

Liebe Grüße

Andy


bummi - Mi 09.01.13 00:18

Brauchst Du das komplette LOG im Speicher, sonst könntest Du zwischendurch in Dateien oder Datenbanken "wegstreamen"


AScomp - Mi 09.01.13 00:27

Ich brauche es zwar nicht unbedingt im Speicher, allerdings ist das "wegspeichern" in eine Datei sehr langsam - und nur wegen eines Logs möchte ich keine Datenbankanbindung integrieren.


Tranx - Mi 09.01.13 00:47

Ich weiß nicht, ob das funktioniert, aber vielleicht versuchst Du es mal mit TList.
Das ist dann bei der Einfügung nicht mehr ganz so einfach, denn Du müsstest Dein Objekt als z.B. Record definieren, der TList dann einen Zeiger auf den Record übergeben, der bei jedem Hinzufügen allerdings erst erzejugt werden muss. Auch das Löschen Der Liste gestaltet sich dann etwas schwieriger, da Du ja jeden Eintrag erst löschen musst, bevor Du die Liste löschst.


Du hättest dann eine Listen nur mit Pointern. Die nehmen weniger Platz weg. Und die Einträge können ja irgendwo im Speicher sein, müssen ja nicht zusammenhängend sein. Vielleicht klappt das für Deinen Fall.


Delphi-Quelltext
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:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:
68:
69:
70:
71:
72:
73:
74:
75:
76:
77:
78:
79:
80:
81:
82:
83:
84:
85:
86:
87:
88:
89:
90:
unit ListeUnit;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  Grids, StdCtrls, Buttons;

type
  TForm1 = class(TForm)
    btnHinzu: TButton;
    btnloeschen: TButton;
    btnanzeigen: TButton;
    ergebnis: TStringGrid;
    btnNeueListe: TBitBtn;
    procedure btnHinzuClick(Sender: TObject);
    procedure btnNeueListeClick(Sender: TObject);
    procedure btnanzeigenClick(Sender: TObject);
    procedure btnloeschenClick(Sender: TObject);
  private
    { Private-Deklarationen }
  public
    { Public-Deklarationen }
  end;

type
    pObjRec = ^ObjRec;
    ObjRec = record
          Dateiname : string;
          Zeitstempel : TDateTime;
          end;
var
  Form1: TForm1;
  Liste : TList;
  Eintrag : pObjRec;

implementation

{$R *.DFM}

procedure TForm1.btnHinzuClick(Sender: TObject);
begin
  New(Eintrag);
  Eintrag^.Dateiname := 'NeuerEintrag '+intToStr(Liste.Count+1);
  Eintrag^.Zeitstempel := Now;
  Liste.Add(Eintrag);
  btnanzeigen.Enabled := True;
end;

procedure TForm1.btnNeueListeClick(Sender: TObject);
begin
  Liste := TList.Create;
  btnNeueListe.Enabled := False;
  btnHinzu.Enabled := True;
  btnloeschen.Enabled := True;
end;

procedure TForm1.btnanzeigenClick(Sender: TObject);

var
  i : Integer;
  Eintr : pObjRec;
begin
  ergebnis.Cells[0,0] := 'Eintragstext';
  ergebnis.Cells[1,0] := 'Datum';
  for i := 0 to Liste.Count-1 do begin
    Eintr := Liste[i];
    ergebnis.Cells[0,i+1] := Eintr^.Dateiname;
    ergebnis.Cells[1,i+1] := DateTimeToStr(Eintr^.Zeitstempel);
  end;
  ergebnis.RowCount := Liste.count+1;
end;

procedure TForm1.btnloeschenClick(Sender: TObject);
var
  i : Integer;
  Eintr : pObjRec;
begin
  for i := 0 to Liste.Count-1 do begin
    Eintr := Liste[i];
    Dispose(Eintr);
  end;
  Liste.Free;
  btnanzeigen.enabled := False;
  btnHinzu.Enabled := False;
  btnloeschen.Enabled := False;
  btnNeueListe.Enabled := True;
end;

end.

(Ein Formular mit mehreren Buttons und einem Stringgrid)


AScomp - Mi 09.01.13 00:53

Danke, das wäre auch eine Idee!

Ich frage mich momentan aber, ob nicht einfach ein Setzen der Capacity auf einen definitiv "sicheren" Wert nicht eine Lösung wäre. Also beim Erstellen der TStringList gleich die Capacity z.B. auf 1000000 setzen:

BkmLogFiles := TStringList.Create;
BkmLogFiles.Capacity := 1000000;

Allerdings frage ich mich, inwiefern Capacity im Voraus schon wissen kann, wieviel Speicherplatz die ja erst später hinzugefügten Objects nachher tatsächlich benötigen.


Gerd Kayser - Mi 09.01.13 00:57

user profile iconAScomp hat folgendes geschrieben Zum zitierten Posting springen:
Da im Voraus nicht klar ist, wie viele Einträge es im Log geben wird, kann ich die Größe schlecht vorher bestimmen.

Wenn es nur darum geht, Fehlermeldungen in eine Logdatei wegzuschreiben, würde ich AssignFile, Rewrite, Append usw. empfehlen. Für das Schreiben der Einträge dann eine entsprechende Prozedur. Zum Beispiel (Auszug):


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
procedure Protokoll(Idx: Integer; Zeile: String);
var
  Temp : String;
begin
  case Idx of
    0: Temp := '-PROG- ';
    1: Temp := '-INIT- ';
    2: Temp := '-IMPORT- ';
    3: Temp := '-KONFIG- ';
    4: Temp := '-ABGLEICH- ';
  end;

  Temp := FormatDateTime('hh:nn:ss.zzz ', now) + Temp;
  Writeln(Protokolldatei, Temp + Zeile);
end;


Ansprechen dann zum Beispiel so:


Delphi-Quelltext
1:
2:
3:
4:
5:
  if not DirectoryExists(Ausgangsverzeichnis) then
    begin
      ForceDirectories(Ausgangsverzeichnis);
      Protokoll(1'Verzeichnis ' + Ausgangsverzeichnis + ' wurde angelegt.');
    end;


AScomp - Mi 09.01.13 01:00

Hallo Gerd,

sorry, hatte gerade die Zitieren-Funktion für PN genutzt und dir wohl privat geantwortet...

Es geht nicht um ein "kleines" Log, sondern wie im ersten Beitrag geschrieben um meist mehrere hundertausend Dateieinträge plus Zeitstempel zu jeder Datei.


Gerd Kayser - Mi 09.01.13 01:04

user profile iconAScomp hat folgendes geschrieben Zum zitierten Posting springen:
Es geht nicht um ein "kleines" Log, sondern wie im ersten Beitrag geschrieben um meist mehrere hundertausend Dateieinträge plus Zeitstempel zu jeder Datei.

Im Initialization-Teil der Unit, die die Protokollprozedur enthält, öffnest Du die Datei, im Finalization-Abschnitt machst Du sie wieder zu. Dann kannst Du doch zigtausende Protokollzeilen schreiben.


Narses - Mi 09.01.13 01:08

Moin!

Was passiert denn mit dem Log? Musst du indiziert zugreifen können? Wenn nicht, brauchst du auch keine Array-Basierte Komponente (übrigens bezweifle ich, dass sich TStringList und TList irgendwie bei diesem Einsatz großartig unterscheiden). Dann wäre eine LinkedList deutlich besser! :idea:

cu
Narses


AScomp - Mi 09.01.13 01:11

Hallo Narses,

ja, ich muss indiziert darauf zugreifen - es muss außerdem sortiert werden (eben, damit ich schneller darauf zugreifen kann).

Darum macht das mit dem direkten Wegschreiben in eine Datei weniger Sinn. Zudem ist das ständige Schreiben auch recht langsam, und obendrein ist es kein "Programmlog", sondern ein "Aufgabenlog", sprich: Es werden mehrere Logs pro Programmsession geschrieben, nicht nur eines.

Mich würde brennend interessieren, wie Capacity genau arbeitet bzw. ob ich damit nicht einfach von vornherein ausreichend Platz für die später hinzugefügten Objects reservieren kann.


Narses - Mi 09.01.13 01:18

Moin!

user profile iconAScomp hat folgendes geschrieben Zum zitierten Posting springen:
Mich würde brennend interessieren, wie Capacity genau arbeitet bzw. ob ich damit nicht einfach von vornherein ausreichend Platz für die später hinzugefügten Objects reservieren kann.
Kurz: .Capacity legt die Blockgröße fest, um die die Liste erweitert wird, wenn diese intern "voll" ist.

Es könnte also durchaus in deinem Fall helfen, einen "geeigneten" Wert für .Capacity zu wählen. Probier´s erstmal mit 2^20 = 1.048.576. :idea:

cu
Narses


AScomp - Mi 09.01.13 02:20

Danke euch, ich werde es erstmal mit Capacity ausprobieren.

Momentan sind nur zwei Anwender davon betroffen, trotzdem hoffe ich, bald ein Feedback zu bekommen, ob die Änderung etwas gebracht hat.

Liebe Grüße

Andy


AScomp - Mi 09.01.13 14:38

Capacity hat leider keine Besserung gebracht - es erscheint nach wie vor eine EOutOfMemory Exception.

Ich bin jetzt dabei, die StringList durch ein Array zu ersetzen, vielleicht funktioniert es damit besser.


thepaine91 - Mi 09.01.13 16:01

Also ich finde die Lösung über eine SQLLite Datenbank am besten.
Die maximale Größe müsste 2Tb betragen und sollte somit problemlos ausreichen.


AScomp - Mi 09.01.13 16:20

Das Array hat letzten Endes genau dasselbe Problem - irgendwann wird es einfach zu groß.

Jetzt wird das Log also doch direkt weggeschrieben in eine Datei. Geht wohl bei der enormen Datenmenge nicht anders (zumindest, wenn man auf eine Datenbank verzichten möchte).


Narses - Mi 09.01.13 16:24

Moin!

user profile iconAScomp hat folgendes geschrieben Zum zitierten Posting springen:
Capacity hat leider keine Besserung gebracht - es erscheint nach wie vor eine EOutOfMemory Exception.
Die spannende (und IMHO entscheidende) Frage ist, bei welcher Aktion und unter welchen Umständen exakt die Problematik auftritt. Das sollte dann ja ein reproduzierbarer UseCase sein. Ich würde dir empfehlen, eine entsprechende Testumgebung/-Fall aufzusetzen, sonst wird das immer Fischen-im-Trüben bleiben. :nixweiss:

user profile iconAScomp hat folgendes geschrieben Zum zitierten Posting springen:
Ich bin jetzt dabei, die StringList durch ein Array zu ersetzen, vielleicht funktioniert es damit besser.
user profile iconAScomp hat folgendes geschrieben Zum zitierten Posting springen:
Das Array hat letzten Endes genau dasselbe Problem - irgendwann wird es einfach zu groß.
Genau das meine ich, liegt es wirklich nur an einer ineffizienten/ungeeigneten Verwaltung/Datenstruktur oder nicht doch an der Datenmenge. Das wirst du nur mit einer Testumgebung/-Fall rauskriegen.

Liegt es an der Datenmenge, ist da kein Kraut gewachsen. Liegt´s an der Verwaltung, sollte man da was geeignetes finden können (z.B. die Daten doch in einer verketteten Liste halten und nur Index-Arrays im Zugriffsfall bilden, dann steht die Datenmenge ja fest und die Verwaltungsstruktur kann exakt alloziert werden). :idea:

cu
Narses


Gerd Kayser - Mi 09.01.13 17:18

user profile iconNarses hat folgendes geschrieben Zum zitierten Posting springen:
Liegt es an der Datenmenge, ist da kein Kraut gewachsen.

Sollte man nicht eher darüber nachdenken, ob und wie man die Datenmenge reduzieren kann?

user profile iconAScomp hat folgendes geschrieben Zum zitierten Posting springen:
mehrere hundertausend Dateieinträge plus Zeitstempel zu jeder Datei.

Da lohnt es sich sicherlich, einige Stunden mal über das Programmdesign nachzudenken.


AScomp - Mi 09.01.13 17:30

@Gerd: Die Datenmenge lässt sich nicht reduzieren - wenn der Anwender 2 TB an Daten wegsichern möchte und ein Protokoll dazu braucht, dann ist die Datenmenge nunmal nicht kleiner zu bekommen. ;)

Das Speichern in einer Datei od. Datenbank ist vermutlich der einzig gangbare Weg, um diese Datenmengen verwalten zu können.

Zwischenzeitlich hatte ich auch über eine Komprimierung der Daten im Speicher nachgedacht, allerdings bringt das insofern wenig, weil der Zeitaufwand für die Komprimierung (und beim Zugriffsbedarf der Dekomprimierung) viel zu hoch wäre.

Mit dem Programmdesign hat das dann letzten Endes auch recht wenig zu tun. ;)

@Narses: Ja, es liegt an der Datenmenge. Das bisherige System lief über Jahre hinweg einwandfrei. Doch inzwischen gibt es immer mehr Anwender, die teilweise Datenmengen im Terabyte-Bereich wegsichern möchten. Das macht ein im Speicher gehaltenes Log natürlich unmöglich, zumal wie oben beschrieben eine Komprimierung nicht in Frage kommt.

P.S. Wobei natürlich nicht die Datenmenge an sich das Problem darstellt, sondern die hohe Anzahl an Dateien/Dateinamen.


jaenicke - Mi 09.01.13 17:39

Also ich kann mit Delphi XE problemlos innerhalb eines Wimpernschlags (paar Millisekunden halt) auch eine Million Zeilen mit je 100 Zeichen in eine TStringList schreiben und in nicht einmal 2 Sekunden in eine am Ende 100 MB große Textdatei schreiben...

Das Problem liegt glaube ich woanders...

// EDIT:
EOutOfMemory kommt z.B. auch bei überschriebenem Speicher vor. Hast du schon einmal FastMM im FullDebugMode benutzt? Vielleicht zeigt das ja etwas an.


AScomp - Mi 09.01.13 17:43

user profile iconjaenicke hat folgendes geschrieben Zum zitierten Posting springen:
Also ich kann mit Delphi XE problemlos innerhalb eines Wimpernschlags (paar Millisekunden halt) auch eine Million Zeilen mit je 100 Zeichen in eine TStringList schreiben und in nicht einmal 2 Sekunden in eine am Ende 100 MB große Textdatei schreiben...

Das Problem liegt glaube ich woanders...

// EDIT:
EOutOfMemory kommt z.B. auch bei überschriebenem Speicher vor. Hast du schon einmal FastMM im FullDebugMode benutzt? Vielleicht zeigt das ja etwas an.


Das ist nicht das Problem. Die Geschwindigkeit ist generell nicht das Problem.

Aber du kannst es recht einfach nachstellen:


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
procedure TForm1.Button1Click(Sender: TObject);
var
  SL: TStringList;
  FileDateValue: TFileDateValue;
  StartTime: DWORD;
  i, ItemsToAdd: integer;
begin
  SL := TStringList.Create;
  ItemsToAdd := StrToInt(Edit1.Text);
  if CheckBox1.Checked then
    SL.Capacity := SL.Count + ItemsToAdd;
  // GetTickCount gives the number of millisecs since Windows started
  StartTime := GetTickCount;
  for i := 1 to ItemsToAdd do begin
    FileDateValue := TFileDateValue.Create;
    FileDateValue.DateTimeValue := Now;
    SL.AddObject('C:\verzeichnis\unterverzeichnis\recht langes unterverzeichnis\der dateiname mit.extension', FileDateValue);
  end;
  Label1.Caption := IntToStr(GetTickCount - StartTime);
  SL.Free;
end;


Du brauchst nur ein Edit, eine CheckBox, einen Button und ein Label. 10 Millionen Einträge sind ganz schnell geschrieben - macht auch fast keinen Unterschied, ob du die Capacity vorher setzt oder nicht. ABER: Es wird eine EOutOfMemory-Exception ausgelöst (in Abhängigkeit vom zur Verfügung stehenden Arbeitsspeicher früher oder später).

Bitte nicht wundern, dass die FileDateValues in diesem Beispiel nicht mehr freigegeben werden. Es ging da nur um ein schnelles Beispiel, dasselbe funktioniert auch ohne FileDateValues.


Gerd Kayser - Mi 09.01.13 17:44

user profile iconAScomp hat folgendes geschrieben Zum zitierten Posting springen:
Die Datenmenge lässt sich nicht reduzieren - wenn der Anwender 2 TB an Daten wegsichern möchte und ein Protokoll dazu braucht, dann ist die Datenmenge nunmal nicht kleiner zu bekommen. ;)

Dann teile doch die Datenmenge auf, z. B. nach jeweils 10.000 Einträgen die StringListe wegschreiben, die StringListe leeren und wieder bis zum 10.000. Eintrag füllen, in neue Datei wegschreiben, leeren usw.

Für Fehlermeldungen könntest Du ja eine extra StringListe verwenden.


AScomp - Mi 09.01.13 17:48

user profile iconGerd Kayser hat folgendes geschrieben Zum zitierten Posting springen:
user profile iconAScomp hat folgendes geschrieben Zum zitierten Posting springen:
Die Datenmenge lässt sich nicht reduzieren - wenn der Anwender 2 TB an Daten wegsichern möchte und ein Protokoll dazu braucht, dann ist die Datenmenge nunmal nicht kleiner zu bekommen. ;)

Dann teile doch die Datenmenge auf, z. B. nach jeweils 10.000 Einträgen die StringListe wegschreiben, die StringListe leeren und wieder bis zum 10.000. Eintrag füllen, wegschreiben, leeren usw.

Für Fehlermeldungen könntest Du ja eine extra StringListe verwenden.


Dann kann ich die Daten aber auch direkt in die Datei schreiben (und diese geöffnet lassen). Ist insofern einfacher, weil sonst ein Teil im Speicher ist, der andere in der Datei.


jaenicke - Mi 09.01.13 17:54

Gut, bei 10 Millionen Einträgen ist das ja auch schon rechnerisch klar... selbst bei nur 100 Zeichen pro Zeile.
10.000.000 Einträge * 100 Zeichen * 2 Byte = 2.000.000.000 Byte

Und ein 32-Bit Programm kann nun einmal nur 2 GiB RAM nutzen, egal was du machst. Bei einer deratigen Datenmenge kannst du niemals alles im Speicher halten. ;-)


AScomp - Mi 09.01.13 18:03

user profile iconjaenicke hat folgendes geschrieben Zum zitierten Posting springen:
Gut, bei 10 Millionen Einträgen ist das ja auch schon rechnerisch klar... selbst bei nur 100 Zeichen pro Zeile.
10.000.000 Einträge * 100 Zeichen * 2 Byte = 2.000.000.000 Byte

Und ein 32-Bit Programm kann nun einmal nur 2 GiB RAM nutzen, egal was du machst. Bei einer deratigen Datenmenge kannst du niemals alles im Speicher halten. ;-)


Ich bin bisher schlicht nicht von so umfangreichen Datenmengen ausgegangen. Aber es kommt jetzt immer häufiger vor, dass ein paar hunderttausend Dateien gesichert werden sollen.

Darum bin ich jetzt auch umgestiegen auf das direkte Wegschreiben in eine Datei. Ist zwar beim Zugriff jetzt etwas schwieriger, da die Daten nicht mehr so leicht zu holen sind wie bisher, aber auf längere Sicht mit Sicherheit von Vorteil.

Danke euch für die Unterstützung! :)


Gerd Kayser - Mi 09.01.13 18:15

Ich würde den Logzeilen einen festen Aufbau geben, dann kann man recht einfach und bequem nach Fehlern suchen. Zum Beispiel so:


Quelltext
1:
lfd. Nummer - Zeitstempel - Fehlercode - Dateiname                    


Wenn die Fehlercodes schön untereinander in die Logdatei geschrieben werden, ist es ein leichtes, z. B. mit Readln diese Zeilen zu ermitteln und anzuzeigen.


AScomp - Mi 09.01.13 18:18

user profile iconGerd Kayser hat folgendes geschrieben Zum zitierten Posting springen:
Ich würde den Logzeilen einen festen Aufbau geben, dann kann man recht einfach und bequem nach Fehlern suchen. Zum Beispiel so:


Quelltext
1:
lfd. Nummer - Zeitstempel - Fehlercode - Dateiname                    


Wenn die Fehlercodes schön untereinander in die Logdatei geschrieben werden, ist es ein leichtes, z. B. mit Readln diese Zeilen zu ermitteln und anzuzeigen.


Es geht hier aber nicht um ein Fehlerlog, sondern um ein Protokoll darüber, welche Dateien mit welchem Zeitstempel gesichert wurden.


Gerd Kayser - Mi 09.01.13 18:23

user profile iconAScomp hat folgendes geschrieben Zum zitierten Posting springen:
Es geht hier aber nicht um ein Fehlerlog.

Ich rede von einem Backup-Log. Wenn Du meinst, daß während der Sicherung keinerlei Probleme auftreten können (Rechteproblem, eklusiv geöffnete Dateien usw.), dann lasse halt die Fehlercodes weg.


AScomp - Mi 09.01.13 18:28

user profile iconGerd Kayser hat folgendes geschrieben Zum zitierten Posting springen:
user profile iconAScomp hat folgendes geschrieben Zum zitierten Posting springen:
Es geht hier aber nicht um ein Fehlerlog.

Ich rede von einem Backup-Log. Wenn Du meinst, daß während der Sicherung keinerlei Probleme auftreten können (Rechteproblem, eklusiv geöffnete Dateien usw.), dann lasse halt die Fehlercodes weg.


Das Fehler-Log ist vom Backup-Log getrennt. Bisher war das gemeinsam in einer StringList, jetzt habe ich es aber getrennt in verschiedene Dateien (weil am Ende sowieso zwei versch. Dateien für beide Logs erstellt werden).

Mir ist klar, wie ich Daten aus einer Datei lesen kann, darum ging es ja nicht. Der Punkt ist nur, dass das Lesen aus einer großen Datei deutlich länger braucht als das Lesen aus dem Speicher. Aber wie gesagt, das Problem ist zufriedenstellend gelöst.


Gerd Kayser - Mi 09.01.13 18:39

user profile iconAScomp hat folgendes geschrieben Zum zitierten Posting springen:
Der Punkt ist nur, dass das Lesen aus einer großen Datei deutlich länger braucht als das Lesen aus dem Speicher.

Da kann man sicherlich einiges optimieren.

Vor ein paar Jahren gab es mal eine Diskussion in einer Newsgroup zum Thema, wie man aus einer Datei größer 100 MB alle Leereichen durch ein anderes Zeichen austauschen kann. Bei den ersten Versuchen dauerte das etwa 20 Minuten. Zum Schluß waren es unter 2 Sekunden, wenn ich die Zahlen noch richtig im Kopf habe.