Seltsames Verhalten bei Optimierung Stufe 2 und 3

Für Fragen zur Programmiersprache auf welcher Lazarus aufbaut
Antworten
Ekkehard
Beiträge: 65
Registriert: So 12. Feb 2023, 12:42
OS, Lazarus, FPC: Windows Lazarus 3.6, FPC 3.2.2
CPU-Target: 64-Bit
Wohnort: Hildesheim

Seltsames Verhalten bei Optimierung Stufe 2 und 3

Beitrag von Ekkehard »

Liebe FPC/Lazarus Gemeinde,
nachdem ich mein Projekt stets im Debug-Modus compiliere, dort pingelig darauf achte, jede Warnung zu verhindern und auch die Hints auf das absolute Mindestmaß zu reduzieren, alle Checks eingeschaltet lasse und auch den heaptrc eingeschaltet habe um frühzeitig Speicherleckagen zu entdecken, war ich entspannt, als ich den Schalter auf "Release" setzte.
Leider hat schlug meine Entspannung schnell um, denn das Programm, welches bisher reibungslos lief, hing einfach fest.

Durch geduldiges Suchen und Probieren, konnte ich jetzt das Problem eingrenzen, aber leider nicht in ein kleines Beispielprogramm auslagern. Meine Versuche, dieses Problem mit diesem Fehler an anderer Stelle nachzubauen, schlugen allesamt fehl.

Die eigentliche Fehlerstelle ist sehr übersichtlich.
Es gibt auch zwei Lösungen, die das Problem umschiffen, die auch sehr übersichtlich sind, aber ich würde gerne verstehen, was da schief geht, um zukünftig mögliche (weitere) Fehler zu umgehen.

Es handelt sich um eine Funktion des schon in anderen Beiträgen erwähnten Quadratur-Baums.
An dieser Stelle geht es darum, durch alle Knoten des Baums zu laufen und in jedem Knoten eine Variable auf 0 zu setzen.
Im Prinzip sieht das an dieser Stelle wie eine verkette Liste aus, die Funktion NextNode holt immer den dem übergebenen Knoten nachfolgenden Knoten und liefert den zurück:

Code: Alles auswählen

var
  qn : TLazQuadNode;
begin
  qn := FRootNode; // weise den Wurzelknoten der lokalen Variablen zu, der Wurzelknoten ist immer <> Nil
  repeat
    qn.FTotalNodeItemsCount := 0; // Setze die Variable im Knoten auf 0
    qn := NextNode(qn); // Hole den nächsten Knoten
  until not Assigned(qn); // wiederhole solange, bis kein weiterer Knoten mehr vorhanden ist
  // Hier kommt weiterer Code
end;
In den Optimierungsstufen 0 und 1 funktioniert das auch reibungslos, in Stufe 2 und 3 wird qn nie Nil, die Schleife wird nicht unterbrochen.
Durch Debuggen, kann ich zeigen, dass die Funktion NextNode(qn) das richtige Ergebnis liefert, allein, das Funktionsergenis wird nicht der Variablen qn zugewiesen.
Somit arbeitet die Schleife immer auf dem gleichen Knoten.

Ich habe dann den Code etwas verändert, mit dem Ziel diese "Nichtzuweisung" beweisen zu können.

Code: Alles auswählen

var
  qn, nn : TLazQuadNode;
begin
  qn := FRootNode; // weise den Wurzelknoten der lokalen Variablen zu, der Wurzelknoten ist immer <> Nil
  repeat
    qn.FTotalNodeItemsCount := 0; // Setze die Variable im Knoten auf 0
    nn := qn; // Merke den aktuellen Knoten
    qn := NextNode(qn); // Hole den nächsten Knoten
    if qn = nn then // wenn aktueller Knoten gleich alter Knoten
      raise Exception.Create('Warum???');  // Erzeuge eine Exception
  until not Assigned(qn); // wiederhole solange, bis kein weiterer Knoten mehr vorhanden ist
  // Hier kommt weiterer Code
end;
Aber zu meiner großen Überraschung funktioniert dieser Code, die Schleife terminiert und es wird keine(!) Exception ausgeführt.

Als Versuch Nummer 3 habe ich dann die Schleife nochmal anders aufgebaut.
Die Funktion NextNode kann nämlich auch mit dem Parameter Nil aufgerufen werden, sie liefert dann den FRootNode zurück.
Dies ausnutzend, habe ich die Schleife so aufgebaut, dass zu erst NextNode aufgerufen wird, dann (nach Prüfung auf Nil) die Zuweisung auf die Variable erfolgt.

Code: Alles auswählen

  
var
  qn : TLazQuadNode;
begin
  qn := Nil;
  repeat
    qn := NextNode(qn); // Hole den nächsten Knoten
    if Assigned(qn) then // Wenn nicht Nil
      qn.FTotalNodeItemsCount := 0; // Setze die Variable im Knoten auf 0
  until not Assigned(qn);  // wiederhole solange, bis kein weiterer Knoten mehr vorhanden ist
  // Hier kommt weiterer Code  
end;  
Und ebenfalls zu meiner großen Überraschung, auch diese Variante funktioniert.

Die Funktion rufe ich testweise direkt von einem Button auf. Versuchsweise habe ich innerhalb der Antwort-Methode des Button unmittelbar vor dem Aufruf der Funktion, die Schleife 1:1 abgebildet:

Code: Alles auswählen

var
//  lTmpFN : String;
  qn : TLazQuadNode;      
begin
  // Schleife exakt wie in der Methode RecountTotalNodeItems
  qn := FTestQuadTree.RootNode;
  repeat
    qn.FTotalNodeItemsCount := 0;
    qn := FTestQuadTree.NextNode(qn);
  until not Assigned(qn);
  FTestQuadTree.RecountTotalNodeItems; // Aufruf der Methode
end;
Und, Überraschung, dort geht die Schleife, während sie eine Etage tiefer, innerhalb der aufgerufenen Funktion nicht geht.

Was veranlasst den Compiler, den ersten Aufbau anders zu behandeln als nachfolgenden?
Ich habe Lazarus 4.3 mit FPC 3.2.2 am Start. Um sicher zu gehen, dass ich nicht irgendwelche alten Geister jage, habe ich die letzte Installation geladen und neuinstalliert und auch "alles aufräumen und compilieren" verwendet.
Über Hinweise wäre ich dankbar.

Benutzeravatar
Zvoni
Beiträge: 378
Registriert: Fr 5. Jul 2024, 08:26
OS, Lazarus, FPC: Windoof 10 Pro (Laz 2.2.2 FPC 3.2.2)
CPU-Target: 32Bit
Wohnort: BW

Re: Seltsames Verhalten bei Optimierung Stufe 2 und 3

Beitrag von Zvoni »

Du sagst, es beginnt mit O2
https://wiki.freepascal.org/Optimization

Muss also irgendwas hiermit zu tun haben:
REMOVEEMPTYPROCS + UNUSEDPARA + REGVAR + STACKFRAME + TAILREC + CSE

Da ich von Compiler keine Ahnung habe (davon aber jede Menge), kann ich nicht weiter helfen....
Ein System sie alle zu knechten, ein Code sie alle zu finden,
Eine IDE sie ins Dunkel zu treiben, und an das Framework ewig zu binden,
Im Lande Redmond, wo die Windows drohn.

Ekkehard
Beiträge: 65
Registriert: So 12. Feb 2023, 12:42
OS, Lazarus, FPC: Windows Lazarus 3.6, FPC 3.2.2
CPU-Target: 64-Bit
Wohnort: Hildesheim

Re: Seltsames Verhalten bei Optimierung Stufe 2 und 3

Beitrag von Ekkehard »

Vielen Dank für die Antwort, die doch den Weg gewiesen hat!

Unterbinde ich mittels

Code: Alles auswählen

{$optimization noREGVAR}
die entsprechende Optimierung geht es.

Im Assembler-Fenster zeigt sich dann, der Unterschied. Und ich lag mit meiner Vermutung, dass einfach das Funktionsergebnis verworfen wird goldrichtig.
Variante geht:

Code: Alles auswählen

(...)\lazQuadTree\Source\ulazQuadTree.pas:3818  qn := FRootNode;
00000001001E5293 488B45F8                 mov rax,[rbp-$08]
00000001001E5297 488B4010                 mov rax,[rax+$10]
00000001001E529B 488945F0                 mov [rbp-$10],rax
00000001001E529F 90                       nop 
(..)\lazQuadTree\Source\ulazQuadTree.pas:3820  qn.FTotalNodeItemsCount := 0; // Set the field variable direct instead of the property
00000001001E52A0 488B45F0                 mov rax,[rbp-$10]
00000001001E52A4 C7809000000000000000     mov [rax+$00000090],$00000000
(..)\lazQuadTree\Source\ulazQuadTree.pas:3821  qn := NextNode(qn);
00000001001E52AE 488B55F0                 mov rdx,[rbp-$10]
00000001001E52B2 488B4DF8                 mov rcx,[rbp-$08]
00000001001E52B6 E815FEFFFF               call -$000001EB    # $00000001001E50D0 NextNode ulazQuadTree.pas:3763
00000001001E52BB 488945F0                 mov [rbp-$10],rax
(..)\lazQuadTree\Source\ulazQuadTree.pas:3822  until not Assigned(qn);
00000001001E52BF 4885C0                   test rax,rax
00000001001E52C2 75DC                     jnz -$24    # $00000001001E52A0 RecountTotalNodeItems+80 ulazQuadTree.pas:3820
Variante geht nicht:

Code: Alles auswählen

(..)\lazQuadTree\Source\ulazQuadTree.pas:3818  qn := FRootNode;
00000001001E5298 488B45F8                 mov rax,[rbp-$08]
00000001001E529C 488B5810                 mov rbx,[rax+$10]
(..)\lazQuadTree\Source\ulazQuadTree.pas:3820  qn.FTotalNodeItemsCount := 0; // Set the field variable direct instead of the property
00000001001E52A0 C7839000000000000000     mov [rbx+$00000090],$00000000
(..)\lazQuadTree\Source\ulazQuadTree.pas:3821  qn := NextNode(qn);
00000001001E52AA 4889DA                   mov rdx,rbx
00000001001E52AD 488B4DF8                 mov rcx,[rbp-$08]
00000001001E52B1 E81AFEFFFF               call -$000001E6    # $00000001001E50D0 NextNode ulazQuadTree.pas:3763
(..)\lazQuadTree\Source\ulazQuadTree.pas:3822  until not Assigned(qn);
00000001001E52B6 4885C0                   test rax,rax
00000001001E52B9 75E5                     jnz -$1B    # $00000001001E52A0 RecountTotalNodeItems+80 ulazQuadTree.pas:3820
hier die Eintragung des Rückgabewertes wegoptimiert wurde, denn die nachfolgende Zeile findet sich vor der Prüfung auf , nicht aber im "optimierten" Code:

Code: Alles auswählen

00000001001E52BB 488945F0                 mov [rbp-$10],rax
Aber warum?
Na gut, für mich als Anwender unlösbar und doch habe ich wieder was gelernt :-)

Benutzeravatar
m.fuchs
Lazarusforum e. V.
Beiträge: 2822
Registriert: Fr 22. Sep 2006, 19:32
OS, Lazarus, FPC: Winux (Lazarus 2.0.10, FPC 3.2.0)
CPU-Target: x86, x64, arm
Wohnort: Berlin
Kontaktdaten:

Re: Seltsames Verhalten bei Optimierung Stufe 2 und 3

Beitrag von m.fuchs »

Ich habe jetzt beim Überfliegen deines (auszugsweisen) Quellcodes keine Rückgabe innerhalb deiner Funktion gesehen.

Gibt es denn eine? Wird die auch in jedem Pfad durchgeführt?
Wird die auch immer beim Aufruf entgegengenommen?
Software, Bibliotheken, Vorträge und mehr: https://www.ypa-software.de

Benutzeravatar
Zvoni
Beiträge: 378
Registriert: Fr 5. Jul 2024, 08:26
OS, Lazarus, FPC: Windoof 10 Pro (Laz 2.2.2 FPC 3.2.2)
CPU-Target: 32Bit
Wohnort: BW

Re: Seltsames Verhalten bei Optimierung Stufe 2 und 3

Beitrag von Zvoni »

m.fuchs hat geschrieben: Di 17. Sep 2024, 15:51 Ich habe jetzt beim Überfliegen deines (auszugsweisen) Quellcodes keine Rückgabe innerhalb deiner Funktion gesehen.

Gibt es denn eine? Wird die auch in jedem Pfad durchgeführt?
Wird die auch immer beim Aufruf entgegengenommen?
Es geht wohl um diese Zeile in seinem ersten Code-Block (der unter O2/O3 nicht funktioniert)

Code: Alles auswählen

qn := NextNode(qn); // Hole den nächsten Knoten
Hast recht: Um es zu verstehen, müssten wir diese Funktion mal sehen
Ein System sie alle zu knechten, ein Code sie alle zu finden,
Eine IDE sie ins Dunkel zu treiben, und an das Framework ewig zu binden,
Im Lande Redmond, wo die Windows drohn.

Ekkehard
Beiträge: 65
Registriert: So 12. Feb 2023, 12:42
OS, Lazarus, FPC: Windows Lazarus 3.6, FPC 3.2.2
CPU-Target: 64-Bit
Wohnort: Hildesheim

Re: Seltsames Verhalten bei Optimierung Stufe 2 und 3

Beitrag von Ekkehard »

Also, die Methode in der der Fehler passiert ist eine Prozedur, liefert also auch kein Ergebnis (Result).
Sie sieht derzeit (inklusive aller (aus-)kommentierten Basteleien) wie nachfolgend aus.
Die einkommentierte Variante funktioniert auch in Optimierung 2+3.

Code: Alles auswählen

procedure TLazQuadTree.RecountTotalNodeItems;
var
  qn : TLazQuadNode;
  cnt, sum : Integer;

begin
  // First step: Iterate to all Nodes and set the
  // FTotalNodeItemsCount field to 0.

  qn := Nil;
  repeat
    qn := NextNode(qn);
    if not Assigned(qn) then Break;
    qn.FTotalNodeItemsCount := 0; // Set the field variable direct instead of the property
  until False;

  //  + REMOVEEMPTYPROCS + UNUSEDPARA + REGVAR + STACKFRAME + TAILREC + CSE
  {.$optimization noREGVAR}
{
// This loop will never terminate
  qn := FRootNode;
  repeat
    qn.FTotalNodeItemsCount := 0; // Set the field variable direct instead of the property
    qn := NextNode(qn);
  until not Assigned(qn);

// This loops terminates and does never raise an exception
  qn := FRootNode;
  repeat
    qn.FTotalNodeItemsCount := 0; // Set the field variable direct instead of the property
    nn := qn;
    qn := NextNode(qn);
    if qn = nn then
      raise Exception.Create('Why???');
  until not Assigned(qn);
}


  // Second step: Iterate to all Nodes and increment the
  // TotalNodeItemsCount property by the UsedNodeItemsCount
  // of each node.
  // Since modifiying the TotalNodeItemsCount updates all the
  // parent nodes up to the RootNode, the summation is done.
  sum := 0;
  qn := Nil; // Kommt der Compiler an dieser Stelle durcheinander, weil er glaubt dass das "Ergebnis" aus der Schleife oben irrelvant ist?
  repeat
    qn := NextNode(qn);
    if not Assigned(qn) then Break;
    cnt := qn.AssignedNodeItemsCount;
    qn.TotalNodeItemsCount := qn.FTotalNodeItemsCount + cnt; // Write to the property, to force the action
    sum := sum + cnt;
  until False;
  if FRootNode.FTotalNodeItemsCount <> sum then
    raise ELQTException.Create('Something went terrible wrong in RecountTotalNodeItems');
end;
Ich vermute mittlerweile, dass der Compiler mit der Anweisung

Code: Alles auswählen

  qn := Nil;
vor der zweiten Schleife durcheinander kommt und denkt, dass er das erste Ergebnis verwerfen kann.

Die Funktion NextNode funktioniert in allen Varianten, dürfte also nicht Teil des Problems sein. Sie enthält, natürlich gänzlich anders formuliert weil ein Baum und keine Liste, aber equivalent, Folgendes:

Code: Alles auswählen

function TLazQuadTree.NextNode(const AStartNode: TLazQuadNode): TLazQuadNode;
begin
  if not Assigned(AStartNode) then
    Exit(FRootNode);
  Result := AStartNode.NextNode;
end;

paweld
Beiträge: 87
Registriert: So 11. Jun 2023, 16:01
OS, Lazarus, FPC: Lazarus trunk, FPC fixes

Re: Seltsames Verhalten bei Optimierung Stufe 2 und 3

Beitrag von paweld »

In FPC 3.2.0 wurde ein Fehler im Zusammenhang mit Schleifen gefunden, die einen Ausnahmefänger in ihrem Block hatten - dies wurde bei Optimierungen auf Stufe 2 und höher festgestellt.
Das veröffentlichte Beispiel bestand aus einer „for“-Schleife und einem „try ... except“ - Ihres hat das nicht, aber Sie haben auch eine Schleife („repeat ... until“) und einen Ausnahmefänger („raise exception“).
Soweit ich mich erinnere, sah der Code, der den Fehler verursachte, wie folgt aus:

Code: Alles auswählen

var
  i: Integer;
begin
  for i := 0 to 10 do
  begin
    try
      //do something
    except
      //do something on exception
    end;
  end;
end;
.
Aber dieser Code verursachte den Fehler nicht mehr:

Code: Alles auswählen

var
  i: Integer;
begin
  try
    for i := 0 to 10 do
    begin
      //do something
    end
  except
    //do something on exception
  end;
end;
.
Es wurde empfohlen, keine auf diese Weise konstruierten Schleifen zu verwenden oder die Optimierung mit Stufe 1 zu nutzen.
Soweit ich mich erinnere, wurde der Fehler in FPC 3.2.2 teilweise behoben, aber er kann in einigen Fällen immer noch auftreten.

Ich konnte keinen Link zu diesem Thread im Lazarus Forum finden, wenn ich ihn finde, werde ich ihn hinzufügen.

Der Grund für das Ausbleiben eines Fehlers im zweiten Fall ist also die Abtrennung der Ausnahme außerhalb der Schleife.
Grüße / Pozdrawiam
paweld

Ekkehard
Beiträge: 65
Registriert: So 12. Feb 2023, 12:42
OS, Lazarus, FPC: Windows Lazarus 3.6, FPC 3.2.2
CPU-Target: 64-Bit
Wohnort: Hildesheim

Re: Seltsames Verhalten bei Optimierung Stufe 2 und 3

Beitrag von Ekkehard »

Mir ist es gelungen den Fehler zu separieren und in ein sehr kleines Programm zu packen.
Man benötigt eine neue leere Formular-Anwendung und einen TButton.
Das Form muss eine FormCreate und eine FormDestroy Antwortmethode haben.
Dann kann man den gesamten nachfolgenden Code oder die entsprechenden Teile davon kopieren.
Die Compilereinstellungen müssen "release" sein (am Einfachsten "Debug" und "Release" in den Compiler-Einstellungen erzeugen), aber zum Debuggen müssen die beiden Häckchen "Run uses Debugger" und "Generate info for the debugger" gesetzt werden.

Man setzt einen Breakpoint auf

Code: Alles auswählen

  until not Assigned(lTest);  

startet das Programm und klickt auf den Button.
Man lässt sich mit "Ansicht->Debuggerfenster->Assembler" den ausführenden Code anzeigen und sieht, dass zwischen dem Funktionsaufruf "NextObj" und der Schleifenprüfung kein Befehl zur Übernahme des Funktionsergebnisses vorhanden ist

Code: Alles auswählen

000000010002E63A E8F1000000               call +$000000F1    # $000000010002E730 NextObj unit1.pas:99
(...)\LinkedListTest\unit1.pas:64  until not Assigned(lTest);
000000010002E63F 4885C0                   test rax,rax
Es gibt drei Varianten das Verhalten zu "reparieren":
1.) Die Optimierung REGVAR ausschalten (bspw. "$optimization noREGVAR" einkommentieren.
2.) die nach der Schleife folgende Anweisung "lTest := Nil;" auskommentieren.
3.) die beiden Zeilen mit der Auslösung einer Exception auskommentieren.

Mit anderen Worten, die Art der Optimierung, die Art der Schleife, das nachfolgende "Nil" der in der Schleife verwendeten Variable und die Exception führen gemeinsam zu dem Fehler.

Kurz noch eine Erläuterung was das Programm macht:
Es baut in FormCreate eine verkettete Liste von 10 Objekten vom Typ TTestObj auf, beginnend mit einem Root-Objekt, diese werden in FormDestroy wieder freigegeben.
In der Antwortfunktion des Button wird die Liste in einer Schleife durchlaufen, in dem jeweils das aktuelle Objekt (beginnend mit dem bekannten Root-Objekt) einer Funktion "NextObj" übergeben wird, die, wenn es keine weiteren Objekte in der Liste gibt, Nil retourniert und so die Schleife zu Abbruch bringt.
In der Schleife wird die Anzahl der durchlaufenen Objekte gezählt und nach der eine Exception ausgelöst, falls die Anzahl 0 ist.

Code: Alles auswählen

unit Unit1;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Forms, Controls, Graphics, Dialogs, StdCtrls;

type

  TTestObj = class(TObject)
  private
    FNext : TTestObj;
  end;

  { TForm1 }

  TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    FRoot : TTestObj;
    function NextObj(const ATestObj : TTestObj) : TTestObj;
  public

  end;

var
  Form1: TForm1;

implementation

{$R *.lfm}

{ TForm1 }

{
  In the release-mode, using Optimization O2 or O3 (including the REGVAR-Option),
  the following method will have an infinite loop.
  The compiler misses to move the return-code from the NextObj-function into
  the rax register. This keeps the default value and subsequently the
  loop is never terminated.

  There are three possibilites to "repair":
  1.) not using the REGVAR optimization (by using the "$optimization noREGVAR" command).
  2.) removing the "lTest := Nil;" command after the loop.
  3.) removing the raise of the Exception.
}
procedure TForm1.Button1Click(Sender: TObject);
var
  lTest : TTestObj;
  cnt : Integer;
begin
  {.$optimization noREGVAR}

  cnt := 0;
  lTest := FRoot;
  repeat
    Inc(cnt);
    lTest := NextObj(lTest);
  until not Assigned(lTest);

  lTest := Nil;
  if cnt = 0 then
    raise Exception.Create('Including this exception causes the compiler fault!');
end;

procedure TForm1.FormCreate(Sender: TObject);
var
  i : Integer;
  lTest : TTestObj;
begin
  FRoot := TTestObj.Create;
  lTest := FRoot;
  for i := 0 to 9 do
  begin
    lTest.FNext := TTestObj.Create;
    lTest := lTest.FNext;
  end;
end;

procedure TForm1.FormDestroy(Sender: TObject);
var
  lTest, lNext : TTestObj;
begin
  lTest := FRoot;
  repeat
    lNext := lTest.FNext;
    lTest.Free;
    lTest := lNext;
  until not Assigned(lTest);
end;

function TForm1.NextObj(const ATestObj: TTestObj): TTestObj;
begin
  Result := ATestObj.FNext;
end;

end.

BeniBela
Beiträge: 321
Registriert: Sa 21. Mär 2009, 17:31
OS, Lazarus, FPC: Linux (Lazarus SVN, FPC 2.4)
CPU-Target: 64 Bit

Re: Seltsames Verhalten bei Optimierung Stufe 2 und 3

Beitrag von BeniBela »

Ekkehard hat geschrieben: Mi 18. Sep 2024, 14:02 Mir ist es gelungen den Fehler zu separieren und in ein sehr kleines Programm zu packen.
tja, hier tritt mit dem Programm derselbe Fehler auf

Ekkehard hat geschrieben: Di 17. Sep 2024, 15:07 Im Assembler-Fenster zeigt sich dann, der Unterschied. Und ich lag mit meiner Vermutung, dass einfach das Funktionsergebnis verworfen wird goldrichtig.
Variante geht:

Code: Alles auswählen

(...)\lazQuadTree\Source\ulazQuadTree.pas:3818  qn := FRootNode;
00000001001E5293 488B45F8                 mov rax,[rbp-$08]
00000001001E5297 488B4010                 mov rax,[rax+$10]
00000001001E529B 488945F0                 mov [rbp-$10],rax
00000001001E529F 90                       nop 
(..)\lazQuadTree\Source\ulazQuadTree.pas:3820  qn.FTotalNodeItemsCount := 0; // Set the field variable direct instead of the property
00000001001E52A0 488B45F0                 mov rax,[rbp-$10]
00000001001E52A4 C7809000000000000000     mov [rax+$00000090],$00000000
(..)\lazQuadTree\Source\ulazQuadTree.pas:3821  qn := NextNode(qn);
00000001001E52AE 488B55F0                 mov rdx,[rbp-$10]
00000001001E52B2 488B4DF8                 mov rcx,[rbp-$08]
00000001001E52B6 E815FEFFFF               call -$000001EB    # $00000001001E50D0 NextNode ulazQuadTree.pas:3763
00000001001E52BB 488945F0                 mov [rbp-$10],rax
(..)\lazQuadTree\Source\ulazQuadTree.pas:3822  until not Assigned(qn);
00000001001E52BF 4885C0                   test rax,rax
00000001001E52C2 75DC                     jnz -$24    # $00000001001E52A0 RecountTotalNodeItems+80 ulazQuadTree.pas:3820
Variante geht nicht:

Code: Alles auswählen

(..)\lazQuadTree\Source\ulazQuadTree.pas:3818  qn := FRootNode;
00000001001E5298 488B45F8                 mov rax,[rbp-$08]
00000001001E529C 488B5810                 mov rbx,[rax+$10]
(..)\lazQuadTree\Source\ulazQuadTree.pas:3820  qn.FTotalNodeItemsCount := 0; // Set the field variable direct instead of the property
00000001001E52A0 C7839000000000000000     mov [rbx+$00000090],$00000000
(..)\lazQuadTree\Source\ulazQuadTree.pas:3821  qn := NextNode(qn);
00000001001E52AA 4889DA                   mov rdx,rbx
00000001001E52AD 488B4DF8                 mov rcx,[rbp-$08]
00000001001E52B1 E81AFEFFFF               call -$000001E6    # $00000001001E50D0 NextNode ulazQuadTree.pas:3763
(..)\lazQuadTree\Source\ulazQuadTree.pas:3822  until not Assigned(qn);
00000001001E52B6 4885C0                   test rax,rax
00000001001E52B9 75E5                     jnz -$1B    # $00000001001E52A0 RecountTotalNodeItems+80 ulazQuadTree.pas:3820
hier die Eintragung des Rückgabewertes wegoptimiert wurde, denn die nachfolgende Zeile findet sich vor der Prüfung auf , nicht aber im "optimierten" Code:

Code: Alles auswählen

00000001001E52BB 488945F0                 mov [rbp-$10],rax
Aber warum?
Die Prüfung auf nil ist nicht das Problem

Der Rückgabewert ist in rax und mit "test rax, rax" ist das im optmierten Code auch korrekt

Nur bei dem nächsten Aufruf von NextNode hat es immer noch den alten Node. Da hat der Optimierer versucht [rbp-$10] durch rbx zu ersetzen
Ekkehard hat geschrieben: Di 17. Sep 2024, 15:07 Na gut, für mich als Anwender unlösbar und doch habe ich wieder was gelernt :-)
Man könnte nach dem Kompilieren mit einem Hexeditor jedes rbx durch rax ersetzen

Benutzeravatar
af0815
Lazarusforum e. V.
Beiträge: 6808
Registriert: So 7. Jan 2007, 10:20
OS, Lazarus, FPC: FPC fixes Lazarus fixes per fpcupdeluxe (win,linux,raspi)
CPU-Target: 32Bit (64Bit)
Wohnort: Burgenland
Kontaktdaten:

Re: Seltsames Verhalten bei Optimierung Stufe 2 und 3

Beitrag von af0815 »

Nur so ein Frage: Jetzt habt ihr das Problem mit den Optimierer so schön isoliert - gibt es dazu einen Bugreport oder ist das vielleicht schon gelöst ?
Blöd kann man ruhig sein, nur zu Helfen muss man sich wissen (oder nachsehen in LazInfos/LazSnippets).

Ekkehard
Beiträge: 65
Registriert: So 12. Feb 2023, 12:42
OS, Lazarus, FPC: Windows Lazarus 3.6, FPC 3.2.2
CPU-Target: 64-Bit
Wohnort: Hildesheim

Re: Seltsames Verhalten bei Optimierung Stufe 2 und 3

Beitrag von Ekkehard »

Nachdem klar war, dass es sich um einen Bug handelt, habe ich ihn bei gitlab gemeldet
https://gitlab.com/freepascal.org/fpc/s ... sues/40920

Zugleich habe ich versucht den Bug deutlicher zu isolieren. Dies ist dahingehend gelungen, dass der Bug innerhalb eines kleinen Kommanzeilenprogramms gezeigt werden kann, welches sich direkt mit dem fpc-Compiler von der Kommandozeile compilieren lässt.
Nachdem ein User dort den Fehler jetzt auch nachvollziehen kann, hoffe ich, dass er sich beheben lässt und in einem Patch oder neuen Release nicht mehr zeigt.
Er tritt nur in der Windows 64bit Version auf und nur wenn die Optimierung REGVAR verwendet wird.

Ich denke, damit ist der Fall hier hinreichend erörtert. Danke!

Rheinländer
Beiträge: 6
Registriert: Di 1. Okt 2024, 13:05

Re: Seltsames Verhalten bei Optimierung Stufe 2 und 3

Beitrag von Rheinländer »

Hallo Ekkehard, der Beitrag ist schon etwas länger her und es wurde ein Bugticket erzeugt. Mir ist bei deiner Demo im Beitrag von 18.9 um 14:02 aufgefallen, dass die private Variabel, beim erzeugen des Objekts TTestObj.FNext nicht initialisiert wird. Dadurch ist beim letzten (also 10ten) Knoten die Variable fNext nicht initiiert. Hier kann irgend etwas drin stehen. Es ist nicht gewährleistet das der Compiler die Initialisierung durchführt. Das kann in der Assigned Anweisung in FormDestroy unterschiedliche Auswirkungen haben. Wie der alte C Programmierer sagt der Zeiger zeigt auf ein nicht deklarierten Speicher.

Dein Beispiel

Code: Alles auswählen

unit Unit1;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Forms, Controls, Graphics, Dialogs, StdCtrls;

type

  TTestObj = class(TObject)
  private
    FNext : TTestObj;
  end;

  { TForm1 }

  TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    FRoot : TTestObj;
    function NextObj(const ATestObj : TTestObj) : TTestObj;
  public

  end;

var
  Form1: TForm1;

implementation

{$R *.lfm}

{ TForm1 }

{
  In the release-mode, using Optimization O2 or O3 (including the REGVAR-Option),
  the following method will have an infinite loop.
  The compiler misses to move the return-code from the NextObj-function into
  the rax register. This keeps the default value and subsequently the
  loop is never terminated.

  There are three possibilites to "repair":
  1.) not using the REGVAR optimization (by using the "$optimization noREGVAR" command).
  2.) removing the "lTest := Nil;" command after the loop.
  3.) removing the raise of the Exception.
}
procedure TForm1.Button1Click(Sender: TObject);
var
  lTest : TTestObj;
  cnt : Integer;
begin
  {.$optimization noREGVAR}

  cnt := 0;
  lTest := FRoot;
  repeat
    Inc(cnt);
    lTest := NextObj(lTest);
  until not Assigned(lTest);

  lTest := Nil;
  if cnt = 0 then
    raise Exception.Create('Including this exception causes the compiler fault!');
end;

procedure TForm1.FormCreate(Sender: TObject);
var
  i : Integer;
  lTest : TTestObj;
begin
  FRoot := TTestObj.Create;
  lTest := FRoot;
  for i := 0 to 9 do
  begin
    lTest.FNext := TTestObj.Create;
    // Initalisierung 
    // ITest.FNext.FNext := nil;
    // Schöner wäre die Initialisierung im Objekt :o)
    lTest := lTest.FNext;
  end;
end;

procedure TForm1.FormDestroy(Sender: TObject);
var
  lTest, lNext : TTestObj;
begin
  lTest := FRoot;
  repeat
    lNext := lTest.FNext;
    lTest.Free;
    lTest := lNext;
  until not Assigned(lTest);
end;

function TForm1.NextObj(const ATestObj: TTestObj): TTestObj;
begin
  Result := ATestObj.FNext;
end;

end.

Ekkehard
Beiträge: 65
Registriert: So 12. Feb 2023, 12:42
OS, Lazarus, FPC: Windows Lazarus 3.6, FPC 3.2.2
CPU-Target: 64-Bit
Wohnort: Hildesheim

Re: Seltsames Verhalten bei Optimierung Stufe 2 und 3

Beitrag von Ekkehard »

Dadurch ist beim letzten (also 10ten) Knoten die Variable fNext nicht initiiert.
Hallo,
wäre das wirklich so, müsste das Programm in FormDestroy regelmäßig auf die Nase fliegen, weil dort darauf vertraut wird, dass FNext des letzten Objektes Nil ist.
Tatsächlich ist das nie der Fall und das hat einen ganz einfachen Grund.
Alle von TObject abgeleitete Klassen rufen im Constructor die Methode InitInstance auf und in dieser Methode wird mittels

Code: Alles auswählen

     fillchar(instance^, InstanceSize, 0);     
der gesamte Datenspeicher, den die Klasse belegt auf 0 gesetzt, also auch die Variable FNext in meiner abgeleiteten Klasse.
Gäbe es das nicht, müsste jede Instanz, die neue Felder einführt einen eigenen Constructor haben und dieser bestünde meistens nur darin die hinzugefügten Feldvariablen zu initialisieren. Aber wie gesagt, dass ist nicht nötig.
Bei records gibt es diesen Automatismus nicht! Und deshalb warnt der Compiler auch, dass dort Variablen nicht initialisiert sind.

Zum eigentlichen Bug: Der aktuelle Status is closed, ich denke also, dass in der nächsten Ausgabe des Compilers das Problem nicht mehr auftauchen wird.

Rheinländer
Beiträge: 6
Registriert: Di 1. Okt 2024, 13:05

Re: Seltsames Verhalten bei Optimierung Stufe 2 und 3

Beitrag von Rheinländer »

Hallo Ekkehard, danke für die ausführliche Information. Ich habe das noch nicht gewusst, dass der angeforderter Speicher von TObject initiiert wird.

Antworten