inline: Sinn?

Für allgemeine Fragen zur Programmierung, welche nicht! direkt mit Lazarus zu tun haben.
Antworten
Ich934
Lazarusforum e. V.
Beiträge: 316
Registriert: So 5. Mai 2019, 16:52
OS, Lazarus, FPC: ArchLinux und Windows mit FPCUPdeluxe (L: 2.0.X, FPC 3.2.0)
CPU-Target: x86_64, i386
Wohnort: Bayreuth

inline: Sinn?

Beitrag von Ich934 »

Hallo,

in dem Prüfen, ob ein String einen Interger darstellt-Thread wird in einigen Code-Beispielen mit inline gearbeitet.

Nach der Dokumentation wird bei der Verwendung von inline der Code dieser Prozedure anstelle des Aufrufes dann im Programm eingesetzt. Was bringt mir das? Das ich keinen Sprung im Programm habe sondern einfach den Code durchlaufe? In der Dokumentation steht aber auch, dass das nur ein Hinweis für den Compiler ist. Sprich der scheint das dann selbst zu entscheiden. Also warum sollte ich das dann machen? Optimiert der Compiler das nicht eh von selbst?

Vielleicht kann mir hier jemand mal einen Hinweis geben.

Für was verwendet ihr das dann? Für kleine Hilfsfunktionen?

Vielen Dank.
Tipp für PostgreSQL: www.pg-forum.de

Timm Thaler
Beiträge: 1224
Registriert: So 20. Mär 2016, 22:14
OS, Lazarus, FPC: Win7-64bit Laz1.9.0 FPC3.1.1 für Win, RPi, AVR embedded
CPU-Target: Raspberry Pi 3

Re: inline: Sinn?

Beitrag von Timm Thaler »

Ich934 hat geschrieben:Für was verwendet ihr das dann? Für kleine Hilfsfunktionen?


Ja, genau, für kleine Hilfsfunktionen.

Zum Beispiel für AVR- Controller.

Mit dem Assembler-Befehl cli schaltet man den globalen Interrupt ap. Man könnte jetzt immer schreiben: asm cli end;

Oder eine Prozedur aufrufen, die asm cli end; enthält, was aber 7 unnötige Takte benötigt.

Oder avr_cli; aus der Unit intrinsics verwenden, die cli direkt in den Code stellt.

Bei größeren Funktionen muss man sich das halt überlegen: Einerseits macht es den Code durch Wegfallen der Sprungbefehle schneller, andererseits braucht es mehr Programmspeicher, sobald die Funktion mehrfach aufgerufen wird.

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

Re: inline: Sinn?

Beitrag von Warf »

Inlining ist eine Compiler Optimierungstechnik, und mit dem inline keyword in Pascal sagst du dem Compiler: Hey, ich bin mir ziemlich sicher das sich inlining hier lohnt.
Am ende ist es immer noch die Entscheidung des Optimizers und das keyword ist eigentlich wirklich nur ein Hinweis und garantiert nix. Ob eine Funktion tatsächlich geinlined werden kann hängt von vielem ab, das einfachste beispiel, eine rekursive funktion die nicht Schwanz-Rekursiv ist kann man einfach nicht inlinen. Genauso kann der Compiler inlinen ohne das das Keyword angegeben wurde, bei C++ z.B. gibt es so so ein keyword gar nicht (es gibt zwar inline, das macht aber was komplett anderes, was zwar indirekt was mit inlining zu tun hat, aber die entscheidung zu inlinen kann man bei C++ nicht direkt beinflussen), und die inlining entscheidung übernimmt voll und ganz der Optimizer.

Inlining hat ein paar Vorteile, zu aller erst fällt der Funktions call weg. Wenn eine funktion aufgerufen wird passiert das folgende:
1. Alle parameter werden auf den Stack gepusht
2. Frame Pointer und Rücksprungaddresse werden auf den Stack gepusht
3. Stack Canary wird auf den stack gepusht (Nur auf modernen Os und Kompilern die das unterstützen)
4. Alle lokalen variablen werden auf den stack gepusht
5. Jump zu funktion

bei einem return wird dann das folgende gemacht:
1. Stack canary wird überprüft um die integrität der Return Addresse zu überprüfen (natürlich nur wenn einer gesetzt wurde)
2. Gemanagede lokale variablen werden deinitialisiert (Strings, arrays, etc.)
3. Stack wird wieder hergestellt mittels Frame-Pointer
4. jump zur return addresse

Bei einem funktionsaufruf wird also gar nicht mal so wenig code drum herum ausgeführt, jetzt nimm mal diese Funktion aus einem meiner projekte:

Code: Alles auswählen

function htonll(host: QWord): QWord; inline;
begin
{$ifdef FPC_BIG_ENDIAN}
  Result := host;
{$else}
  Result := SwapEndian(host);
{$endif}
end;

Definitiv zu unleserlich das ich die 10 mal in einer funktion haben will, aber im worst case ist das eine instruction (ein load), und dafür dann die 9 operationen oben auszuführen, würde die laufzeit mehr als verzehnfachen.

Der nächste Vorteil ist das Inlining andere Optimierungen unterstützen kann, z.b. sagen wir mal du hast 2 funktionen, die eine schreibt zu erst einen wert in eine variable und die zweite funktion liest dann diesen wert aus der variable. Die funktionen werden direkt nacheinander ausgeführt:

Code: Alles auswählen

procedure fun1(obj: TSomething);
begin
  obj.x := 2;
end;
function fun2(obj: TSomething): Integer;
begin
  Result := obj.x * 2;
end;

Nach dem inlinen sieht das dann so aus:

Code: Alles auswählen

obj.x := 2;
  other := obj.x * 2;

was dann so optimiert werden kann:

Code: Alles auswählen

obj.x := 2;
  other := 4;

Und zack man ist ne multiplikation und nen load losgeworden. Natürlich geht das nur wenn der Optimizer beweisen kann das in der zeit niemand auf das Objekt zugreifen kann (z.b. wenns ne lokale variable ist).
Oder:

Code: Alles auswählen

procedure fun1(obj: TSomething);
begin
  obj.y := 2;
  if obj.x < 0 then
    obj.y *= 5;
end;
procedure fun2(obj: TSomething);
begin
  if obj.y > 5 then
    // irgendwas
end;

sieht nach dem inlinen so aus:

Code: Alles auswählen

 
  obj.y := 2;
  if obj.x < 0 then
    obj.y *= 5;
  if obj.y > 5 then
    // irgendwas

Was dann optimiert werden kann zu:

Code: Alles auswählen

 
  obj.y := 2;
  if obj.x < 0 then
  begin
    obj.y *= 5;
    // irgendwas
  end;

Und du bist ein komplettes if losgeworden

Noch viel interresannter wird das natürlich bei OOP. Klassen wie StringList erzeugt man oftmals um eine funktion wie delimitedText darauf auszuführen und sie danach wieder zu zerstören. Wenn Methoden geinlined werden, könnte der Optimizer z.b. das initialisieren der kompletten StringList überspringen, und nur die für diese operation nötigen Felder initialisieren, was mindestens eine allokation sparen würde (und allokationen sind teuer). Allerdings ist der FPC optimizer nicht so schlau, und ich weiß nicht ob der das wirklich macht. Ist aber was was durch inlining möglich wäre.
Noch krasser ist das bei C++, grade in zusammenhang mit LLVM. Da können dir zum teil ganze klassen weggeinlined werden, z.b. sehr beliebt ist die verwendung einer Factory um Objekte zu erstellen, welche dann cachen kann, und alle daten für ein objekt sammelt, bevor das erzeugt werden kann. Factories werden meist komplett geinlined, weil die ja nix sind als ein wrapper auf die eigentliche konstruktion. Allerdings ist C++ sowohl vom sprachdesign, als auch von den toolchains viel mehr auf Optimierung ausgelegt.

Natürlich ist inlining nicht nur gut. Der hauptnachteil ist ganz einfach, dein Programm wird größer. Das ist auf modernen rechnern mit GB weise ram und internetleitungen von MBit bis GBit natürlich eher weniger relevant, und wenn du ein hochauflösendes Logo oder so in deiner Software hast, braucht das wahrscheinlich um größenordnungen mehr speicher als das Inlining von Funktionen. Auf microprozessoren sieht das aber ganz anders aus. Während du auf Desktops problemlos Funktionen inlinen könntest die mehrere duzende Zeilen lang sind, wenn es dir denn performance gibt, ist es auf der Microcontroller genau anders rum, da büst du lieber etwas geschwindigkeit ein, um dafür nicht die hälfte deines Verfügbaren speichers (sind oft ja nur ein paar KB-MB) an inlining zu verlieren.

Wenn eine Funktionalität nur an einer stelle benutzt wird, die aber so komplex ist das man sie in einer eigenen funktion auslagern will (z.b. irgendwelche komplexen formeln, ein bestimmter teil eines Protokols, etc.), hat inlining eigentlich nur vorteile, du sparst dir den Funktionsaufruf und der speicherverbrauch ist auch nicht höher (sogar niedriger) weil du die Funktion nur an einer stelle benutzt.
Ich habe gesagt das das eigentlich keine Nachteile hat, denn einen Nachteil hat inlining auf jeden fall, das mag der Debugger gar nicht. Mit inline wird selbst auf nidrigen Optimierungsstufen geinlined, was heist das instructions ausgeführt werden der keine direkte representierung mehr im Code haben, weshalb das setzen von breakpoints in geinlineten funktion sowie auch das step by step durchgehen oftmals gar nicht wirklich funktioniert, und manchmal in sehr seltsamen, unnachvollziehbaren jumps endet.
Daher kann ich nur empfehlen während dem debuggen kein inline zu benutzen. Du kannst z.b. in der debug build konfiguration ein Flag definieren und dann sowas machen:

Code: Alles auswählen

procedure foo; {$ifndef DEBUG}inline;{$ENDIF}


Mal als kleine benchmark, bei den isInt funktionen hab ich durch inline etwa 30% speedup rausgeholt


So zu guter letzt, wann sollte man inlinen:
Erstmal, beim debuggen gar nicht. Für den release build dann lohnt es sich immer bei funktionen die sehr dumm sind (also dumme rechnungen ausgelagert sind, erzeugen von objekten mit bestimmten parametern, alles was keine komplexe logik enthält)
Oder wenn du große Funktionen hast, die du in viele kleinere Funktionen aufteilst. Es gilt als gute faustregel das die meisten Funktionen nicht mehr als 20 zeilen lang sein sollten. Wenn du mehr Zeilen in einer Funktion hast lohnt es sich sehr oft diese Funktion in mehrere Funktionen aufzuteilen, und durch inlining hast du keinen laufzeitnachteil dar durch.
Beispiel:

Code: Alles auswählen

function CreateRecieverThread(const ACommunicator: TWebsocketCommunincator):
TWebsocketRecieverThread; inline;
var
  pool: TRecieverThreadPool;
begin
  pool := RecieverThreadPool.Lock;
  try
    Result := pool.GetObject;
    Result.Communicator := ACommunicator;
    Result.Restart;
  finally
    RecieverThreadPool.Unlock;
  end;
end;

Diese funktion wird nur an einer einzigen stelle bei meinem Projekt benutzt

Code: Alles auswählen

procedure TWebsocketHandlerThread.DoExecute;
var
  Recv: TWebsocketRecieverThread;
begin
  Recv := CreateRecieverThread(FCommunicator);
  try
    try
      FHandler.DoHandleCommunication(FCommunicator);
    finally
      FCommunicator.Close;
      FCommunicator.Free;
    end;
  finally
    Recv.Kill;
  end;
end;

Und ich hätte ihn auch problemlos an diese Stelle schreiben können, aber ich denke es ist ziemlich offensichtlich das beide funktionen deutlich lesbarer sind das ich es ausgelagert habe, und wie gesagt, kein laufzeit unterschied dank inlining.

Etwas off topic dazu, für ein projekt habe ich demletzt in den code von FPCUPDeluxe gesehen, und Holla die Waldfee ist dieser code unleserlich. So macht da z.b. die funktion die fpcup aufruft auch alle sanity checks, was einfach hunderte zeilen code sind, die in einer separaten funktion genauso gut funktioniert hätten (wenn nicht sogar besser) und es wäre deutlich lesbarer.
Also wenn du in deinen Code schaust und eine funktion mit mehr als 20 zeilen hast (begin, end, try, finally, etc nicht mitgezählt), solltest du auf jeden Fall drüber nachdenken Teile davon per inline auszulagern. Manchmal macht es sinn große Funktionen zu haben und bis zu 100 zeilen ist das jetzt auch nicht sonderlich kritisch solang alle teile logisch dazu gehören, aber ab 100 zeilen wird es nur schlimmer. Um beim Beispiel FPCUPDeluxe zu bleiben, wenn eine funktion: 1. werte aus dem GUI und defaults aus config files einliest, 2. deren integrität prüft und 3. die an ein anderes Objekt weiter gibt, sehe ich keinen grund warum das keine drei Funktionen sind statt einer.

Ich934
Lazarusforum e. V.
Beiträge: 316
Registriert: So 5. Mai 2019, 16:52
OS, Lazarus, FPC: ArchLinux und Windows mit FPCUPdeluxe (L: 2.0.X, FPC 3.2.0)
CPU-Target: x86_64, i386
Wohnort: Bayreuth

Re: inline: Sinn?

Beitrag von Ich934 »

Vielen Dank für die Erklärung. Ich kannte das bisher nicht und das gibt mir ganz neue Möglichkeiten. Ich muss da jetzt erst mal eine Nacht drüber schlafen und das dann einfach mal probieren, aber ich hab hier schon einige Ideen, wo ich das wirklich sinnvoll einsetzen könnte.

Und ja, ich hab auch so einige Codeblöcke, die einfach lang werden und die man damit ziemlich verkürzen könnte...
Tipp für PostgreSQL: www.pg-forum.de

Antworten