![]() |
Delphi-Version: 10 Seattle
Den Leak bei rekursiven Closures bekämpfen
Zum Abend noch etwas eher Esoterisches:
Ein schlauer Kerl hatte hier nie die Speicherleck-Prüfung an. Ich schalte sie ein und sehe dass dieses Konstrukt hier jedes mal eine
Delphi-Quellcode:
-Instanz und eine
TPrinter
Delphi-Quellcode:
-Methode leaked. Letztere ist rekursiv und erhöht somit ihren eigenen Referenzzähler. Sie selbst hat aber natürlich auch eine Referenz auf den
myClosure
Delphi-Quellcode:
weshalb dieser ebenfalls nie tot geht.
IPrinter
Zumindest vermute ich das, denn ich kenne keinen Weg im Debugger die Closure oder von dieser gecapturte Variablen zu untersuchen. Dafür spricht auch wenn ich, wohl durch die Rekursion, den Referenzzähler der anonymen Methode von Hand um eins verringere- Das ist die letzte (auskommentierte) Zeile.
Delphi-Quellcode:
type
IPrinter = interface procedure write(const value: Integer); end; TPrinter = class(TInterfacedObject, IPrinter) public procedure write(const value: Integer); end; procedure p(); var myClosure: TProc; myCounter: Integer; myPrinter: IPrinter; begin myCounter := 10; myPrinter := TPrinter.Create(); myClosure := procedure() begin myPrinter.write(myCounter); Dec(myCounter); if (myCounter > 0) then myClosure(); end; myClosure(); //IInterface( Pointer(@myClosure)^ )._Release(); end; Meine Frage: Kann man das so machen? Sollte der Compiler das eines Tages ändern und den Referenzzähler bei Rekursion nicht erhöhen fliegt mir das ja wohl zur Laufzeit um die Ohren. Das Beispiel ist natürlich stark simplifiziert und wirkt wahrscheinlich übertrieben aufwändig. Und ja, das bekommt man auch ganz klassisch ohne anonyme Methoden hin: Das ist jetzt meine Übergangslösung 8-) |
AW: Den Leak bei rekursiven Closures bekämpfen
Hallo,
ich hab den Code aus Interesse mal in eine Konsolenanwendung gepackt, aber mit ReportMemoryLeaksOnShutdown auf True zeigt er bei mir keine Leaks an, erst wenn ich myPrinter als TPrinter deklariere, was ja auch Sinn macht. Version: Delphi 10 Seattle.
Delphi-Quellcode:
program Project1;
{$APPTYPE CONSOLE} {$R *.res} uses System.SysUtils; type IPrinter = interface procedure write(const value: Integer); end; TPrinter = class(TInterfacedObject, IPrinter) public procedure write(const value: Integer); end; procedure TPrinter.write(const value: Integer); begin System.Write('Wert: '); System.Writeln(value); end; procedure p(); var myClosure: TProc; myCounter: Integer; myPrinter: IPrinter; begin myCounter := 10; myPrinter := TPrinter.Create(); myClosure := procedure() begin myPrinter.write(myCounter); Dec(myCounter); if (myCounter > 0) then myClosure(); end; myClosure(); end; begin ReportMemoryLeaksOnShutdown:=true; p(); Readln; end. |
AW: Den Leak bei rekursiven Closures bekämpfen
Tut mir leid, ich habe nicht erwähnt dass es nicht beim Delphis mitgeliefertem Memory-Manager sondern erst beim
![]() Da der "volle" FastMM auch genauso viele Leaks findet wie oft p() aufgerufen wurde denke ich dass die Leaks wirklich da sind. Zur Sicherheit rufe ich p() morgen ein paar Milliarden mal auf und schaue auf den Speicherverbrauch 8-) PS: Wie konntest du nur die Implementation von TPrinter erraten :-D |
AW: Den Leak bei rekursiven Closures bekämpfen
Okay damit findet Delphi die Memory Leaks.
Das Ganze scheint sogar zu passieren, wenn man zwei anonyme Methoden in derselben Methode deklariert und sie sich untereinander aufrufen, siehe ![]() Ich hab testweise eine der Lösungen ausprobiert (nach dem Aufruf myClosure:=nil setzen) und damit entstehen die Leaks nicht mehr. Eine bessere Lösung scheint es für einen solchen rekursiven Aufruf nicht zu geben. Gruß, Simon |
AW: Den Leak bei rekursiven Closures bekämpfen
Ich ... Ich war mir sicher dass ich das "Auf nil setzen" als erstes probiert hatte und es keine Abhilfe brachte. Hier in diesem Beispiel tut es das aber einwandfrei. Tolle Sache, vielen Dank!
Ich probiere morgen noch einmal mein Echtwelt-Beispiel aus, ob es da auch hilft. |
AW: Den Leak bei rekursiven Closures bekämpfen
Nicht immer lässt sich das lösen. Wir haben daher einige Stellen mit RegisterExpectedMemoryLeak registriert, so dass diese in der Statistik nicht mehr auftauchen. Wir haben das dabei immer so implementiert, dass diese registrierten Leaks nicht während der Programmlaufzeit zunehmen, sondern lediglich einmal auftauchen. Deshalb können wir diese problemlos ignorieren.
Wenn natürlich dort dann noch Instanzen von Interfaces drinhängen, wird es komplizierter... |
AW: Den Leak bei rekursiven Closures bekämpfen
Zitat:
Wie wäre es mit:
Delphi-Quellcode:
Mavarik
procedure p();
var myClosure: TProc; myCounter: Integer; myPrinter: IPrinter; begin myCounter := 10; myPrinter := TPrinter.Create(); myClosure := procedure() begin myPrinter.write(myCounter); Dec(myCounter); if (myCounter > 0) then myClosure(); myPrinter := NIL; end; myClosure(); end; PS.: OT:Voll die Seuche, wer verwendet den Tabs im Source... |
AW: Den Leak bei rekursiven Closures bekämpfen
Zitat:
|
AW: Den Leak bei rekursiven Closures bekämpfen
Zitat:
Zitat:
![]() |
AW: Den Leak bei rekursiven Closures bekämpfen
Das mit dem IPrinter ist komplett irrelevant für das Beispiel, hier ist der minimale Testcase:
Delphi-Quellcode:
Wie man sehen kann, ist der RefCount von p auf 2. Das ist aber nicht etwa der Fall, weil p sich selbst captured,
program Project1;
{$APPTYPE CONSOLE} uses FastMM4, SysUtils; procedure Main; var p: TProc; count: Integer; begin p := procedure begin inc(count); if count < 3 then p(); end; p(); Writeln((IInterface((@p)^) as TInterfacedObject).RefCount); p := nil; end; begin ReportMemoryLeaksOnShutdown := True; Main; Readln; end. sondern weil der Compiler hier eine implizite interface Variable für die anonyme Methode anlegt. Das kann man sehen, wenn man sich den Prolog der Prozedur anschaut, der wie folgt aussieht:
Code:
Und dann die zuweisung auf p:
Project1.dpr.13: begin
00411E70 55 push ebp 00411E71 8BEC mov ebp,esp 00411E73 83C4F8 add esp,-$08 00411E76 33C0 xor eax,eax 00411E78 8945F8 mov [ebp-$08],eax 00411E7B 33C0 xor eax,eax 00411E7D 55 push ebp 00411E7E 68151F4100 push $00411f15 00411E83 64FF30 push dword ptr fs:[eax] 00411E86 648920 mov fs:[eax],esp 00411E89 B201 mov dl,$01 00411E8B A17C1D4100 mov eax,[$00411d7c] 00411E90 E85730FFFF call TObject.Create 00411E95 8945FC mov [ebp-$04],eax 00411E98 8D45F8 lea eax,[ebp-$08] 00411E9B 8B55FC mov edx,[ebp-$04] 00411E9E 85D2 test edx,edx 00411EA0 7403 jz $00411ea5 00411EA2 83EAF8 sub edx,-$08 00411EA5 E82267FFFF call @IntfCopy
Code:
Woher kommt also das Leak?
Project1.dpr.14: p :=
00411EAA 8B45FC mov eax,[ebp-$04] 00411EAD 83C00C add eax,$0c 00411EB0 8B55FC mov edx,[ebp-$04] 00411EB3 85D2 test edx,edx 00411EB5 7403 jz $00411eba 00411EB7 83EAEC sub edx,-$14 00411EBA E80D67FFFF call @IntfCopy Schauen wir uns den Epilog der Prozedur an - hier mit Rekursion:
Code:
Einmal IntfClear, das ist für die vom Compiler im Prolog implizit angelegte Variable. Aber wo ist das IntfClear für p?
Project1.dpr.24: end;
00411EFF 33C0 xor eax,eax 00411F01 5A pop edx 00411F02 59 pop ecx 00411F03 59 pop ecx 00411F04 648910 mov fs:[eax],edx 00411F07 681C1F4100 push $00411f1c 00411F0C 8D45F8 lea eax,[ebp-$08] 00411F0F E8A066FFFF call @IntfClear 00411F14 C3 ret 00411F15 E94638FFFF jmp @HandleFinally 00411F1A EBF0 jmp $00411f0c 00411F1C 59 pop ecx 00411F1D 59 pop ecx 00411F1E 5D pop ebp 00411F1F C3 ret Dazu muss man wissen, wie das capturen von Variablen vom Compiler behandelt wird. Diese befindet sich nämlich nun als Feld in dem vom Compiler gebauten Objekt für die anonyme Methode und wird somit nicht mehr als lokale Variable behandelt (die sonst hier auch gecleared würde). Schauen wir uns mal den Epilog ohne Rekursion an (der aufruf von p innerhalb von p auskommentiert):
Code:
Siehe da, 2mal IntfClear, einmal für p und das zweite mal für die vom Compiler generierte Variable. Das führt dann zum Freigeben der Instanz.
Project1.dpr.24: end;
00411E99 33C0 xor eax,eax 00411E9B 5A pop edx 00411E9C 59 pop ecx 00411E9D 59 pop ecx 00411E9E 648910 mov fs:[eax],edx 00411EA1 68BE1E4100 push $00411ebe 00411EA6 8D45F4 lea eax,[ebp-$0c] 00411EA9 E80667FFFF call @IntfClear 00411EAE 8D45FC lea eax,[ebp-$04] 00411EB1 E8FE66FFFF call @IntfClear 00411EB6 C3 ret 00411EB7 E9A438FFFF jmp @HandleFinally 00411EBC EBE8 jmp $00411ea6 00411EBE 8BE5 mov esp,ebp 00411EC0 5D pop ebp 00411EC1 C3 ret 00411EC2 8BC0 mov eax,eax |
Alle Zeitangaben in WEZ +1. Es ist jetzt 11:23 Uhr. |
Powered by vBulletin® Copyright ©2000 - 2025, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024-2025 by Thomas Breitkreuz