Create and forget: Referenzzählung für Objekte

Zur Vorstellung von Komponenten und Units für Lazarus
Benutzeravatar
Jorg3000
Lazarusforum e. V.
Beiträge: 168
Registriert: So 10. Okt 2021, 10:24
OS, Lazarus, FPC: Win64
Wohnort: NRW

Create and forget: Referenzzählung für Objekte

Beitrag von Jorg3000 »

Moin!

Jedes Objekt, das man erzeugt, muss man in Pascal manuell wieder freigeben, ist mein bisheriger Kenntnisstand.
Ich habe mal über einen Lösungsansatz per IInterface-Referenzzählung gelesen, konnte damit aber spontan wenig anfangen. War mir zu undurchsichtig.
Und zu Delphi hatte ich als Phrase gefunden: "Automatic reference counting (ARC) for classes is supported by LLVM-based Delphi compilers", aber dazu bislang keine weiteren Details.

Stattdessen habe ich mir nun eigene Gedanken zu Referenzzählung für Objekte gemacht ... und dabei herausgekommen ist eine Unit von mir, mit Beispielprojekt und einer kleinen Webseite ...
https://www.html-file.org/pascal/reference-counting/de/
Sourcecode siehe auf Unterseite "Dokumentation und Beispiel".

EDIT: Update der Unit, siehe hier Forum Seite 2
Grüße, Jörg
Zuletzt geändert von Jorg3000 am Di 26. Apr 2022, 08:33, insgesamt 1-mal geändert.

Soner
Beiträge: 623
Registriert: Do 27. Sep 2012, 00:07
OS, Lazarus, FPC: Win10Pro-64Bit, Immer letzte Lazarus Release mit SVN-Fixes
CPU-Target: x86_64-win64
Wohnort: Hamburg

Re: Create and forget: Referenzzählung für Objekte

Beitrag von Soner »

Jorg3000 hat geschrieben:
So 24. Apr 2022, 09:51
Moin!

Jedes Objekt, das man erzeugt, muss man in Pascal manuell wieder freigeben, ist mein bisheriger Kenntnisstand.
...
Nein, nur für Objekte die kein Eigentümer(Owner) haben, wie z.B. TList, TStringList. Objekte mit Eigentümer sieht man an dem Konstruktor:

Code: Alles auswählen

constructor Create(TheOwner: TComponent);
Solche Objekte werden auch nur freigegeben, wenn der Eigentümer freigegeben werden, meistens passiert das am Ende des Programms.

Ich gebe in meinem Programmen Objekte nach Benutzung frei, das würde ich auch jedem empfehlen.

Diese ganze Referenzzählung und automatische Freigabe haben unter anderem dazu geführt, dass wir trotz Intel Core i5/i7 mit 16 GB RAM mehrere Kerne lahme Programme haben.
Vergleiche Office 2000/2002 mit aktuellem Office.

Warf
Beiträge: 1908
Registriert: Di 23. Sep 2014, 17:46
OS, Lazarus, FPC: Win10 | Linux
CPU-Target: x86_64

Re: Create and forget: Referenzzählung für Objekte

Beitrag von Warf »

Im englischen Forum gab es vor nicht allzulanger zeit mal einen Beitrag mit einer Implementierung die ganz gut aussah: https://forum.lazarus.freepascal.org/in ... 306.0.html

Grundsätzlich gibt es theoretisch 2 Möglichkeiten Referenzzählung zu Implementieren, COM interfaces die von haus aus referenzgezählt sind, oder Managed Records bei denen man das Assignment sowie Initialisierung und Finalisierung selbst überladen kann.

Allerdings ist meines wissens nach https://gitlab.com/freepascal.org/fpc/s ... sues/37164 immer noch nicht gefixt, weshalb Managed records noch nicht wirklich benutzbar sind. Da muss man leider auf Interfaces Zurückgreifen, was etwas doof ist, da die ein zwischenobjekt bilden, sodass man immer 2 mal Pointer dereferenzieren muss (durch virtuelle Methoden wahrscheinlich sogar 3) statt 1 mal, für jeden zugriff, und dass immer 2 Create und Free aufgerufen werden mussen.

Alternativ, kannst du einfach wo möglich Interfaces benutzen, die erlauben auch noch andere coole sachen wie Operator überladen. Schönes beispiel dafür ist die GMP:

Code: Alles auswählen

var a, b: MPInteger;
begin
  a := 1024; // erstellt ein neues GMP objekt und setzt den wert 1024
  b := a ** a; // erstellt ein neues GMP objekt und setzt den wert auf a^a
  ShowMessage(b); // konvertiert b nach string
  // weil a und b Interfaces sind werden sie automatisch gefreed
end;
Das doofe dabei ist leider nur das du praktisch für jede Klasse ein interface erstellen musst (ich wollte irgendwann mal tatsächlich einen code generator schreiben der das automatisch macht) aber wenn du dann nur über dieses Interface zugreifst musst du dich gar nicht mehr um Memory Management kümmern.

Ich benutze Interfaces generell häufiger an stellen an denen es einfach keine Sinnige ownership beziehung gibt. Wenn ein objekt tatsächlich von mehreren Objekten verwendet wird und nicht klar ist welches objekt davon am längsten lebt, ist das der einfachste weg.

Ansonsten sollte man immer versuchen zu überlegen wer der tatsächliche owner eines Objekts ist. Und fest definierte ownership übergaben zu implementieren falls der Owner wechseln sollte. Aber das ist leider nicht immer möglich. Beispiel, in einem (ungerichtet)zyklischen Graphen wo ein Knoten mehrere vorgängerknoten haben kann ist das nicht unbedingt klar, hier muss man eigentlich immer zählen wie viele eltern knoten es gibt und wenn die Zahl auf 0 fällt, und der Knoten unereichbar ist, kann er gefreed werden. Da ist Referenzzählung eine sehr gute option (wobei das eventuell duch tiefliegende Zyklen auch problematisch werden kann)
Soner hat geschrieben:
So 24. Apr 2022, 11:25
Diese ganze Referenzzählung und automatische Freigabe haben unter anderem dazu geführt, dass wir trotz Intel Core i5/i7 mit 16 GB RAM mehrere Kerne lahme Programme haben.
Vergleiche Office 2000/2002 mit aktuellem Office.
Das ist eine ziemlich steile These. Klar, wenn man in einem Inneren loop assignments hat, aber wir reden hier von einem Increment, Dekrement und einer if-abfrage pro assignment, also wenn den programm nicht nur aus assignments besteht glaube ich kaum das das einen bemerkbaren Effekt hat.

Vor allem das Office langsamer geworden ist wegen Referenzzählung halte ich für ziemlich unwahrscheinlich, ich kenne zwar die Code-Basis von Office nicht, würde aber mal davon ausgehen das a. der Kern immernoch gleich ist, und b. die Performanceprobleme vor allem von der massiven anzahl neuer Features die seit dem dazu kamen kommt. Office 2000 hat nur einen Bruchteil der Funktionalität moderner Office versionen.
Hier ist auch der Vergleich zu Oberon schön, der neusten Sprache von Wirth, Oberon ist Garbage Collected (und damit hat es massiv höhere Performance Ansprüche als Referenzzählung) und trozdem ist die Oberon umgebung, die gleichzeitig ein komplettes Betriebsystem ist, extrem schnell. Der Grund dafür ist das die keinen großen Schnick Schnack drin hat, sonder einfach nur minimale Kernfunktionalität, und siehe da, Performance ist absolut kein Problem.

Ich möchte dran erinnern das der FPC und Delphi seit Jahrzehnten Referenzzählung bei Strings und Arrays macht, und ich würde mal ganz dreist behaupten das Strings mehr verwendet werden als Objektpointer.
Das Faszinierende wenn dieses Thema aufkommt finde ich ist das es viele gibt die der Meinung sind Referenzgezählte Objekte braucht man nicht, aber ich noch nie einen Pascal Programmierer gefunden habe der der Meinung ist das Arrays und Strings schlecht sind und stattdessen C-Style Pointer wie PChar verwendet.

MMn. gibt es absolut keinen Grund warum man Free per Hand aufrufen müsste. Strings und Arrays machen es vor, und absolut niemand beschwert sich hier. Wenn man massive performance braucht, sodass Referenzzählung tatsächlich den Unterschied macht (ich war schon einmal an diesem Punkt) sollte man eh keine Klassen benutzen, da die Ganzen Vorteile von klassen (Inheritance, Virtuelle Methoden, RTTI, etc.) alle samt nicht auf high performance ausgelegt sind. Dann sind Records mit cutom Allocatoren eh was man eigentlich machen will

Benutzeravatar
Jorg3000
Lazarusforum e. V.
Beiträge: 168
Registriert: So 10. Okt 2021, 10:24
OS, Lazarus, FPC: Win64
Wohnort: NRW

Re: Create and forget: Referenzzählung für Objekte

Beitrag von Jorg3000 »

Hi!
Danke für die Antworten!
Meine Lösung per Managed Record (siehe meine Webseite) hat kein Problem mit dem Bug der doppelten Finalisierung. :)
Ich möchte auch nichts an Objekten ändern, die einen Owner haben und wo das Freigeben quasi automatisch und problemlos funktioniert.

Meine Unit ist auch kein Performance-Fresser. Nach meiner Vorstellung braucht die Zählung nur für bestimmte Objekte verwendet werden, wo ein manuelles Free schwierig wird. Habe ich auf meiner Webseite erklärt.
Und für ein lahmes Microsoft Office kann ich nun wirklich nichts. :lol:

Vielleicht gibt es noch jemanden, der trotzdem Bedarf an Referenzzählung in bestimmten Fällen hätte, und mein Beispielprojekt ausprobieren möchte.
Grüße, Jörg
Zuletzt geändert von Jorg3000 am So 24. Apr 2022, 18:15, insgesamt 1-mal geändert.

Warf
Beiträge: 1908
Registriert: Di 23. Sep 2014, 17:46
OS, Lazarus, FPC: Win10 | Linux
CPU-Target: x86_64

Re: Create and forget: Referenzzählung für Objekte

Beitrag von Warf »

Jorg3000 hat geschrieben:
So 24. Apr 2022, 14:13
Danke für die Antworten!
Meine Lösung per Managed Record (siehe meine Webseite) hat kein Problem mit dem Bug der doppelten Finalisierung. :)
Das problem ist das du das nicht garantieren kannst. Dieser Doppelte Finalisierungs bug habe ich nur bei Konstruktoren und bei GetEnumerator festgestellt (das heist aber nicht das er ausschließlich da auftritt). Du kannst zwar sagen das du die nicht benutzt, aber was ist wenn der Record in einem anderen record verwendet wird der das benutzt?

Das ist das Problem bei diesem Bug, der kann dir irgendwann an einer ganz anderen stelle um die Ohren fliegen, in einer ganz anderen Unit, in einem ganz anderen typen, weil das ganze von einem anderen typen verwendet wird in einer Funktion die diesen Bug auslöst. Es würde halt vorraussetzen das nicht nur dein typ, sondern jeder der deinen Typen irgendwann verwendet sowie jeder der einen typen der deinen typen irgendwann verwendet, etc. nie in diesen Bug läuft.

Vor allem ist nicht klar wo er überall auftritt, bei Konsturktoren und Enumeratoren scheint es immer der Fall zu sein, aber wie sieht es aus mit anderen situationen, eventuell ist das sogar vom Optimierungslevel oder inlining abbhängig, sodass der Code problemlös läuft, aber man dann eine Zeile wo ganz anders hinzufügt, wodurch sich inlining ändert und plötzlich kracht es.
Dieser bug ist extrem gefährlich. Von daher sind Managed records (die sich auf Finalise verlassen) bis dieser bug gefixt komplett unbenutzbar, da du nie wissen kannst wie der record am ende benutzt wird, oder wann er auftritt.

Benutzeravatar
Jorg3000
Lazarusforum e. V.
Beiträge: 168
Registriert: So 10. Okt 2021, 10:24
OS, Lazarus, FPC: Win64
Wohnort: NRW

Re: Create and forget: Referenzzählung für Objekte

Beitrag von Jorg3000 »

Wie gesagt, das Problem gibt es in meiner Unit nicht.
Denn egal wie oft die Finalisierung auf mein Record ausgeführt wird, wirkt es sich nicht negativ aus, da ich bereits beim ersten Finalize im Record alles brav auf nil setze.
Wir brauchen hier kein Problem breitzutreten, das meine Unit gar nicht betrifft.
Grüße, Jörg

Warf
Beiträge: 1908
Registriert: Di 23. Sep 2014, 17:46
OS, Lazarus, FPC: Win10 | Linux
CPU-Target: x86_64

Re: Create and forget: Referenzzählung für Objekte

Beitrag von Warf »

Ich glaub du verstehst den bug nicht so ganz, es ist nicht so das am ende zwei finalise durchgeführt werden, sondern das ein finalise durchgeführt wird bevor die daten noch verwendet werden. D.h. du benutzt das teil, und während noch referenzen darauf sind wird es auf nil gesetzt wird:

Beispiel:

Code: Alles auswählen

  { TTest }

  TTest = record
    Data: PInteger;
    constructor Create(AData: Integer);
    class operator Finalize(var t: TTEst);
  end;

{ TTest }

constructor TTest.Create(AData: Integer);
begin
  New(Data);
  Data^:=AData;
end;

class operator TTest.Finalize(var t: TTEst);
begin
  if not Assigned(t.Data) then
    Exit;  
  Dispose(t.Data);
  t.Data := nil;
end;

var
  t: TTest;
begin
  t := TTest.Create(42);
  WriteLn(t.Data^); 
Wirft ne Access violation, obwohl data auf nil gesetzt wird und auf nil getestet wird.

Langer Rede kurzer Sinn, managed records sind aktuell unbenutzbar

Benutzeravatar
Jorg3000
Lazarusforum e. V.
Beiträge: 168
Registriert: So 10. Okt 2021, 10:24
OS, Lazarus, FPC: Win64
Wohnort: NRW

Re: Create and forget: Referenzzählung für Objekte

Beitrag von Jorg3000 »

Aber ich benutze keinen Record-constructor Create, sondern einen class operator Initialize.
In meinem Beispiel gibt es keine Access Violation - und das Beispiel funktioniert ganz hervorragend :)
... auch dort, wo der Record als lokale Variable verwendet wird (wo der Speicherbereich nicht vorher mit Nullen gefüllt wird).
Hast du es mit meinem Beispiel ausprobiert?
Danke für deine Hinweise und Bedenken, aber bitte keine Probleme behaupten, die nichts mit meiner Unit zu tun haben.
Grüße, Jörg

Warf
Beiträge: 1908
Registriert: Di 23. Sep 2014, 17:46
OS, Lazarus, FPC: Win10 | Linux
CPU-Target: x86_64

Re: Create and forget: Referenzzählung für Objekte

Beitrag von Warf »

Das problem ist, du magst create nicht benutzen, aber sobald irgendjemand einen record benutzt der deinen record benutzt, und da ein create benutzt geht das genauso kaputt:

Code: Alles auswählen

  { TTest }

  TTest = record
    Data: PInteger;
    class operator Finalize(var t: TTest);
    class operator Initialize(var t: TTest);
    class operator AddRef(var t: TTest);
    class operator Copy(constref src: TTest; var dst: TTest);
  end;

  { TTest2 }

  TTest2 = record
    Data: TTest;
    constructor Create(AData: Integer);
  end;

{ TTest2 }

constructor TTest2.Create(AData: Integer);
begin
  Data.Data := GetMem(SizeOf(Integer) * 2);
  Data.Data[0] := 1; // Ref counting
  Data.Data[1] := AData;
end;


{ TTest }

class operator TTest.Finalize(var t: TTest);
begin
  if Assigned(t.Data) then
  begin
    Freemem(t.Data);
    t.Data := nil;
  end;
end;

class operator TTest.Initialize(var t: TTest);
begin
  t.Data:=nil;
end;

class operator TTest.AddRef(var t: TTest);
begin
  if Assigned(t.Data) then
  begin
    Inc(t.Data[0]);
  end;
end;

class operator TTest.Copy(constref src: TTest; var dst: TTest);
begin
  if Assigned(dst.Data) then
  begin
    dec(dst.Data[0]);
    if dst.Data[0] <= 0 then
      Freemem(dst.Data);
    dst.Data := src.Data;   
    if Assigned(dst.Data) then
      Inc(dst.Data[0]);
  end;
end;

var
  t: TTest2;
begin
  t := TTest2.Create(42);
  WriteLn(t.Data.Data[1]);
end. 
TTest benutzt keine konstruktor, wird aber in TTest2 benutzt, trozdem geht es kaputt

Benutzeravatar
Jorg3000
Lazarusforum e. V.
Beiträge: 168
Registriert: So 10. Okt 2021, 10:24
OS, Lazarus, FPC: Win64
Wohnort: NRW

Re: Create and forget: Referenzzählung für Objekte

Beitrag von Jorg3000 »

Na ja gut, dann biete ich meine Unit mit dem Hinweis an ...

Betten Sie den Referenzzähler-Record derzeit nicht einen anderen Record ein, welcher Create benutzt. :D

... bis dieser fpc-Bug behoben ist https://gitlab.com/freepascal.org/fpc/s ... sues/37164

Mit dieser Einschränkung kann ich leben, solange meine Unit selbst keinen Bug hat und außerhalb des o.g. Sonderfalls richtig funktioniert.
Grüße, Jörg

Warf
Beiträge: 1908
Registriert: Di 23. Sep 2014, 17:46
OS, Lazarus, FPC: Win10 | Linux
CPU-Target: x86_64

Re: Create and forget: Referenzzählung für Objekte

Beitrag von Warf »

Das Problem ist halt das es nicht nur der Konstruktor ist, ich weiß von mindestens einem anderen Fall noch, wenn der Record als Enumerator verwendet wird
Das problem ist, es gibt aktuell keinerlei Informationen was den Bug verursacht, es kann sein das der noch bei vielen anderen situationen auftritt, eventuell verschiedenen CPU Targets, optimierungsstufen, etc. es kann sein das der selbe Code bei einem Funktioniert aber nicht bei jemand anderem.

PS: ich hab versucht das mit deinem Record zu reproduzieren, allerdings kam ich nur zu dem ergebnis das dein Code gar nicht freed und nen memory loch überbleibt:

Code: Alles auswählen

type
  TRefCountedSL = specialize TGenericRefCountingRec<TStringList>;

{ TTest }

procedure DoTest;
var
  t: TRefCountedSL;
begin
  t.Obj := TStringList.Create;
  WriteLn(t.Obj.Text);
end;

begin
  DOTest;
end.   
Die StringList wird nie gefreed

Benutzeravatar
Jorg3000
Lazarusforum e. V.
Beiträge: 168
Registriert: So 10. Okt 2021, 10:24
OS, Lazarus, FPC: Win64
Wohnort: NRW

Re: Create and forget: Referenzzählung für Objekte

Beitrag von Jorg3000 »

Hm, in meinem Beispielprojekt mit meiner Beispiel-Klasse TMyData wird ganz klar Destroy durchgeführt, wie das ShowMessage('Destroying') in .Destroy anzeigt.

Ich verwende FPC 3.2.0 (Lazarus 2.0.10) auf Windows 10, 64 Bit

PS: Danke für deine Geduld und Mühe!
Ich würde mich freuen, wenn meine Unit so brauchbar wäre, wie ich es heute morgen noch gedacht habe. :lol:

Warf
Beiträge: 1908
Registriert: Di 23. Sep 2014, 17:46
OS, Lazarus, FPC: Win10 | Linux
CPU-Target: x86_64

Re: Create and forget: Referenzzählung für Objekte

Beitrag von Warf »

Es wird seltsam:

Code: Alles auswählen

 TTest = class
    destructor Destroy; override;
  end;
  TRefCountedTest = specialize TGenericRefCountingRec<TTest>;
  TRefCountedSL = specialize TGenericRefCountingRec<TStringList>;

destructor TTest.Destroy;
begin
  WriteLn('Destroy');
  inherited Destroy;

end;

procedure TestTest;
var
  t: TRefCountedTest;
begin
  t.Obj := TTest.Create;
end; 

procedure TestSL;
var
  t: TRefCountedSL;
begin
  t.Obj := TStringList.Create;
end;
end;

begin
  TestTest;
  TestSL;
end.           
Heaptrc:

Code: Alles auswählen

Destroy
Heap dump by heaptrc unit of C:\Users\frederic\Desktop\test\test.exe
6 memory blocks allocated : 140/160
4 memory blocks freed     : 104/120
2 unfreed memory blocks : 36
True heap size : 294912 (2448 used in System startup)
True free heap : 292208
Should be : 292232
Call trace for block $0152F848 size 12
  $00424200 of ReferenceCountingForObjects.pas
  $004240D5 of ReferenceCountingForObjects.pas
  $00401B06  TESTSL,  line 37 of test.pas
  $00401B42  main,  line 42 of test.pas
  $BAADF00D
  $BAADF00D
  $BAADF00D
  $BAADF00D
  $BAADF00D
  $BAADF00D
  $BAADF00D
  $BAADF00D
  $BAADF00D
  $BAADF00D
  $BAADF00D
  $BAADF00D
Call trace for block $0152F7C8 size 24
  $004241D0 of ReferenceCountingForObjects.pas
  $004240D5 of ReferenceCountingForObjects.pas
  $00401B06  TESTSL,  line 37 of test.pas
  $00401B42  main,  line 42 of test.pas
  $BAADF00D
  $BAADF00D
  $BAADF00D
  $BAADF00D
  $BAADF00D
  $BAADF00D
  $BAADF00D
  $BAADF00D
  $BAADF00D
  $BAADF00D
  $BAADF00D
  $BAADF00D
Also TTest klappt, TStringList nicht. Hab auch in deinen Code gesehen, hab nix falsches auf anhieb gefunden. Außer das du vergessen hast den Management AddRef zu überladen (der wird aufgerufen wenn das objekt als parameter übergeben wird), das kann aber nicht der Grund für diesen Fehler sein

Benutzeravatar
Jorg3000
Lazarusforum e. V.
Beiträge: 168
Registriert: So 10. Okt 2021, 10:24
OS, Lazarus, FPC: Win64
Wohnort: NRW

Re: Create and forget: Referenzzählung für Objekte

Beitrag von Jorg3000 »

AddRef habe ich bewusst nicht überladen, weil es nach meinem Verständnis keine Auswirkung hat.
Würde ich dabei den Referenzzähler hochzählen, bräuchte ich auch eine Gegenaktion wie SubtractRef o.ä., aber ich glaube das gibt es nicht.

Und dass die StringList nicht freigegeben wird, im Gegensatz zur eigenen Klasse, ist wirklich spannend. Dazu fällt mir gerade nichts ein. Eigentlich sollte ein Destroy ein Destroy sein, egal bei welcher Klasse.
Grüße, Jörg

Warf
Beiträge: 1908
Registriert: Di 23. Sep 2014, 17:46
OS, Lazarus, FPC: Win10 | Linux
CPU-Target: x86_64

Re: Create and forget: Referenzzählung für Objekte

Beitrag von Warf »

Zu dem Memoryleak, keine ahnung was das war, grade mal frisches projekt gemacht, exakt selber code, kein memoryleak mehr

Zu AddRef, das gegenstück zu AddRef ist auch Finalize:

Code: Alles auswählen

 type

  { TTest }

  TTest = record
    class operator initialize(var t: TTest);
    class operator AddRef(var t: TTest);
    class operator finalize(var t: TTest);
  end;

{ TTest }

class operator TTest.initialize(var t: TTest);
begin
  writeLn('init');
end;

class operator TTest.AddRef(var t: TTest);
begin
  writeLn('addref');
end;

class operator TTest.finalize(var t: TTest);
begin
  writeLn('final');
end;

procedure Test2(A: TTest);
begin
end;

procedure Test;
var
  t: TTEst;
begin
  Test2(t);
  WriteLn('Ende Test');
end; 
Ausgabe:

Code: Alles auswählen

init
addref
final
Ende Test
final

Antworten