Warum sollte Code aktiv versuchen, Tail-Call-Optimierung zu verhindern?

Lesezeit: 7 Minuten

Benutzeravatar von JustSid
JustSid

Der Titel der Frage mag etwas seltsam sein, aber die Sache ist, dass meines Wissens überhaupt nichts gegen Tail-Call-Optimierung spricht. Allerdings bin ich beim Durchstöbern von Open-Source-Projekten schon auf einige Funktionen gestoßen, die aktiv versuchen, den Compiler daran zu hindern, eine Tail-Call-Optimierung vorzunehmen, zum Beispiel die Implementierung von CFRunLoopRef der voll davon ist hackt. Zum Beispiel:

static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__() __attribute__((noinline));
static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(CFRunLoopObserverCallBack func, CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    if (func) {
        func(observer, activity, info);
    }
    getpid(); // thwart tail-call optimization
}

Ich würde gerne wissen, warum dies scheinbar so wichtig ist und ob es Fälle gibt, in denen ich als normal Entwickler sollten dies auch beachten? Z.B. Gibt es häufige Fallstricke bei der Tail-Call-Optimierung?

  • Eine mögliche Falle könnte sein, dass eine Anwendung auf mehreren Plattformen reibungslos funktioniert und dann plötzlich nicht mehr funktioniert, wenn sie mit einem Compiler kompiliert wird, der keine Tail-Call-Optimierung unterstützt. Denken Sie daran, dass diese Optimierung eigentlich nicht nur die Performance steigern, sondern Laufzeitfehler (Stack Overflows) verhindern kann.

    – Niklas B.

    28. Mai 2012 um 21:51 Uhr


  • @NiklasB. Aber ist das nicht ein Grund dazu nicht versuchen, es zu deaktivieren?

    – JustSid

    28. Mai 2012 um 21:54 Uhr

  • Ein Systemaufruf könnte ein sicherer Weg sein, die Gesamtbetriebskosten zu senken, aber auch ein ziemlich teurer.

    – Fred Foo

    28. Mai 2012 um 21:54 Uhr

  • Dies ist ein großartiger lehrbarer Moment für das richtige Kommentieren. +1 für die teilweise Erklärung, warum diese Zeile vorhanden ist (um Tail-Call-Optimierung zu verhindern), -100 für die Nichterklärung, warum die Tail-Call-Optimierung überhaupt deaktiviert werden musste …

    – Mark Sowul

    28. Mai 2012 um 22:09 Uhr

  • Da der Wert von getpid() nicht verwendet wird, könnte es nicht von einem informierten Optimierer entfernt werden (da getpid ist eine Funktion, von der bekannt ist, dass sie keine Nebenwirkungen hat), sodass der Compiler trotzdem eine Tail-Call-Optimierung durchführen kann? Dies scheint ein Ja wirklich zerbrechlicher Mechanismus.

    – luiskubal

    28. Mai 2012 um 23:26 Uhr

Benutzeravatar von mattjgalloway
mattjgalloway

Meine Vermutung hier ist, dass es dafür sorgen soll __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ befindet sich zu Debugging-Zwecken im Stack-Trace. Es hat __attribute__((no inline)) was diese Idee unterstützt.

Wenn Sie bemerken, dass diese Funktion sowieso zu einer anderen Funktion springt und springt, ist es also eine Art Trampolin, von dem ich nur denken kann, dass es mit einem so ausführlichen Namen vorhanden ist, um das Debuggen zu unterstützen. Dies wäre besonders hilfreich, wenn man bedenkt, dass die Funktion einen Funktionszeiger aufruft, der von woanders registriert wurde, und daher diese Funktion möglicherweise keine Debugging-Symbole zur Verfügung hat, auf die zugegriffen werden kann.

Beachten Sie auch die anderen ähnlich benannten Funktionen, die ähnliche Dinge tun – es sieht wirklich so aus, als ob es dazu da wäre, um zu sehen, was aus einem Backtrace passiert ist. Denken Sie daran, dass dies der Kerncode von Mac OS X ist und auch in Absturzberichten und Prozessbeispielberichten angezeigt wird.

  • Ja, das stimmt mit __attribute__((noinline)). Ich denke, Sie sind hier genau richtig.

    – Niklas B.

    28. Mai 2012 um 22:04 Uhr


  • Ja, macht in der Tat Sinn. Aber wenn Sie schauen, von wo diese Funktionen aufgerufen werden, werden Sie sehen, dass sie immer nur von einer Funktion aufgerufen werden, zum Beispiel wird meine Beispielfunktion nur von aufgerufen __CFRunLoopDoObservers was definitiv im Stacktrace auftaucht …

    – JustSid

    28. Mai 2012 um 22:21 Uhr

  • Sicher, aber ich denke, es ist ein weiterer Marker für exakt wo der Beobachter Callback / Block / etc ausgeführt wird.

    – mattjgalloway

    28. Mai 2012 um 22:25 Uhr

  • Ich denke, das ist die beste Antwort. +1

    – R.. GitHub HÖR AUF, EIS ZU HELFEN

    28. Mai 2012 um 23:43 Uhr

  • @R .. Ich kann jedoch nur eine Antwort akzeptieren, und Andrew White nannte auch andere Fälle, in denen die Tail-Call-Optimierung möglicherweise nicht erwünscht ist. Denken Sie daran, ich habe nicht gefragt, warum die Funktion dies tut, sondern warum es im Allgemeinen nicht erwünscht sein könnte, und die Funktion als Beispiel aus der Praxis angegeben.

    – JustSid

    29. Mai 2012 um 1:38 Uhr

Dies ist nur eine Vermutung, aber vielleicht, um eine Endlosschleife zu vermeiden, anstatt mit einem Stapelüberlauffehler zu bombardieren.

Da die fragliche Methode nichts auf den Stack legt, scheint es möglich, dass die Tail-Call-Rekursionsoptimierung Code erzeugt, der in eine Endlosschleife eintritt, im Gegensatz zum nicht optimierten Code, der die Rücksprungadresse auf den Stack legen würde die bei Missbrauch schließlich überlaufen würden.

Der einzige andere Gedanke, den ich habe, bezieht sich auf die Beibehaltung der Aufrufe auf dem Stack für das Debugging und den Stacktrace-Druck.

  • Ich denke, die Stacktrace-/Debugging-Erklärung ist viel wahrscheinlicher (und ich wollte sie gerade posten). Eine Endlosschleife ist nicht wirklich schlimmer als ein Absturz, da der Benutzer das Beenden der Anwendung erzwingen kann. Das würde auch die noinline erklären.

    – ughoavgfhw

    28. Mai 2012 um 21:59 Uhr


  • @ughoavgfhw: vielleicht, aber wenn man sich mit Threading und so beschäftigt, sind Endlosschleifen wirklich schwer aufzuspüren. Ich war schon immer der Meinung, dass Missbrauch eine Ausnahme auslösen sollte. Da ich das noch nie machen musste, ist das nur eine Vermutung.

    – Andreas Weiß

    28. Mai 2012 um 22:02 Uhr

  • Synchronizität, irgendwie … Ich bin gerade auf einen schlimmen Fehler gestoßen, der dazu führte, dass eine Anwendung neue Fenster öffnete. Dies lässt mich denken, wenn die Anwendung abgestürzt wäre, bevor sie versucht hätte, “den Haufen” (mein Gedächtnis) zu sättigen und X zu ersticken, hätte ich nicht zum Terminal wechseln müssen, um die verrückte App abrupt zu beenden (da X bald anfing zu werden reagiert nicht). Vielleicht wäre es also ein Grund, den “Fail Fast” -Ansatz zu bevorzugen, der mit einem Stapelüberlauf und ohne Optimierung einhergehen könnte …? oder vielleicht ist es auch nur eine andere Sache…!

    – Shin Takezou

    28. Mai 2012 um 22:03 Uhr

  • @AndrewWhite Hmm, ich liebe Endlosschleifen total – mir fällt nichts ein, was einfacher zu debuggen ist, ich meine, Sie können einfach Ihren Debugger anhängen und die genaue Position und den Zustand des Problems abrufen, ohne zu raten. Aber wenn Sie Stacktraces von Benutzern erhalten möchten, stimme ich zu, dass eine Endlosschleife problematisch ist, also erscheint das logisch – ein Fehler wird in Ihrem Protokoll erscheinen, eine Endlosschleife nicht.

    – Voo

    28. Mai 2012 um 23:16 Uhr


  • Dies setzt voraus, dass die Funktion überhaupt rekursiv ist – ist es aber nicht; weder direkt noch (durch Betrachten des Kontexts, aus dem die Funktion stammt) indirekt. Ich habe anfangs die gleiche falsche Annahme gemacht.

    – Konrad Rudolf

    29. Mai 2012 um 10:50 Uhr

Ein möglicher Grund besteht darin, Debugging und Profilerstellung einfacher zu machen (mit TCO verschwindet der übergeordnete Stack-Frame, wodurch Stack-Traces schwieriger zu verstehen sind).

  • Es ist jedoch etwas seltsam, die Profilerstellung auf Kosten einer Verlangsamung des Programms zu vereinfachen. Es ist genauso sinnvoll wie das Verdünnen Ihres Öls, bevor Sie messen, wie weit Ihr Auto fahren kann: x

    – Matthias M.

    29. Mai 2012 um 7:45 Uhr

  • @MatthieuM.: So etwas würde keinen Sinn machen, wenn der hinzugefügte Aufruf millionenfach in einer Schleife ausgeführt würde, aber wenn er einige hundert Mal pro Sekunde oder weniger ausgeführt wird, ist es möglicherweise besser, ihn im realen System zu belassen und in der Lage sein, zu untersuchen, wie sich das reale System verhält, als es herauszunehmen und zu riskieren, dass eine solche Entfernung eine subtile, aber wichtige Änderung des Systemverhaltens bewirkt.

    – Superkatze

    24. Februar 2015 um 0:26 Uhr

  • @MatthieuM. Wenn das Verdünnen deines Öls überhaupt die Voraussetzung für eine Messung ist, dann macht es eigentlich durchaus Sinn.

    – Dmitri Grigorjew

    25. November 2016 um 10:44 Uhr

  • @DmitryGrigoryev: Nein, tut es nicht. Keine Maßnahme ist lästig, aber eine falsche Maßnahme ist von nutzlos bis gefährlich (je nachdem, wie viel Vertrauen man ihr entgegenbringt). Fortsetzung der Ölanalogie: Wenn es Sie verlangsamt, erhalten Sie möglicherweise Messungen, die darauf hinweisen, dass das Gewicht wichtiger ist als die Aerodynamik, und somit Gewicht entfernen und die Aerodynamik verschlechtern, um das zu optimieren, was Sie gemessen haben … jedoch mit echtem Öl, Wenn Sie schneller fahren, stellt sich heraus, dass die Aerodynamik wichtiger war und Ihre “Verbesserung” schlimmer ist, als nichts zu tun!

    – Matthias M.

    25. November 2016 um 12:55 Uhr

  • @MatthieuM. Kennen Sie die Unschärferelation? Jede Messung ist bis zu einem gewissen Grad falsch, da es keine Möglichkeit gibt, etwas zu messen, ohne mit dem zu messenden Objekt zu interagieren. Selbst wenn Sie das Öl in Ihrem Beispiel nicht ändern, wird die Instrumentierung des Autos die Aerodynamik trotzdem verändern.

    – Dmitri Grigorjew

    25. November 2016 um 13:01 Uhr


1418440cookie-checkWarum sollte Code aktiv versuchen, Tail-Call-Optimierung zu verhindern?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy