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

80

Der Titel der Frage mag etwas seltsam sein, aber die Sache ist, dass meines Wissens nichts gegen die Optimierung von Tail Calls spricht. Beim Durchsuchen von Open Source-Projekten bin ich jedoch bereits auf einige Funktionen gestoßen, die aktiv versuchen, den Compiler daran zu hindern, eine Tail-Call-Optimierung durchzuführen, beispielsweise die Implementierung von CFRunLoopRef, das voll von solchen Hacks ist . 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 gibt es Fälle, in denen ich als normaler Entwickler dies auch beachten sollte? Z.B. Gibt es häufige Fallstricke bei der Tail-Call-Optimierung?

JustSid
quelle
10
Eine mögliche Gefahr 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 die Tail-Call-Optimierung nicht unterstützt. Denken Sie daran, dass diese Optimierung nicht nur die Leistung steigern, sondern auch Laufzeitfehler (Stapelüberläufe) verhindern kann.
Niklas B.
5
@ NiklasB. Aber ist das nicht ein Grund, nicht zu versuchen, es zu deaktivieren?
JustSid
4
Ein Systemaufruf kann ein sicherer Weg sein, um die Gesamtbetriebskosten zu erhöhen, aber auch ein ziemlich teurer.
Fred Foo
39
Dies ist ein großartiger lehrbarer Moment für das richtige Kommentieren. +1, um teilweise zu erklären, warum diese Leitung vorhanden ist (um die Tail-Call-Optimierung zu verhindern), -100, um nicht zu erklären, warum die Tail-Call-Optimierung überhaupt deaktiviert werden musste ...
Mark Sowul
16
Da der Wert von getpid()nicht verwendet wird, kann er nicht von einem informierten Optimierer entfernt werden (da getpideine Funktion bekanntermaßen keine Nebenwirkungen hat), sodass der Compiler trotzdem eine Tail-Call-Optimierung durchführen kann? Dies scheint ein wirklich fragiler Mechanismus zu sein.
luiscubal

Antworten:

82

Ich vermute hier, dass es sicherstellen soll, dass es sich zu __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__Debugging-Zwecken im Stack-Trace befindet. Es hat __attribute__((no inline))was diese Idee stützt.

Wenn Sie bemerken, dass diese Funktion ohnehin nur zu einer anderen Funktion wechselt, handelt es sich also um eine Form des Trampolins, von der ich nur glauben kann, dass sie einen so ausführlichen Namen enthält, um das Debuggen zu erleichtern. Dies wäre besonders hilfreich, da die Funktion einen Funktionszeiger aufruft, der von einer anderen Stelle registriert wurde, und daher auf diese Funktion möglicherweise keine Debugging-Symbole zugreifen können.

Beachten Sie auch die anderen ähnlich benannten Funktionen, die ähnliche Dinge tun - es sieht wirklich so aus, als ob sie dazu beitragen, zu sehen, was aus einer Rückverfolgung passiert ist. Beachten Sie, dass dies der Kerncode von Mac OS X ist und auch in Absturzberichten angezeigt und Beispielberichte verarbeitet werden.

mattjgalloway
quelle
Ja, das stimmt mit überein __attribute__((noinline)). Ich denke, Sie sind hier genau richtig.
Niklas B.
Ja, macht in der Tat Sinn. Wenn Sie jedoch nachsehen, woher diese Funktionen aufgerufen werden, werden Sie feststellen, dass sie immer nur von einer Funktion aufgerufen werden. Beispielsweise wird nur meine Beispielfunktion aufgerufen, __CFRunLoopDoObserversdie definitiv in der Stapelverfolgung
angezeigt wird
1
Sicher, aber ich denke, es ist ein weiterer Marker dafür , wo genau der Beobachter-Rückruf / Block / etc ausgeführt wird.
Mattjgalloway
2
Ich denke, das ist die beste Antwort. +1
R .. GitHub STOP HELPING ICE
@R .. Ich kann jedoch nur eine Antwort akzeptieren und Andrew White nannte auch andere Fälle, in denen eine Tail-Call-Optimierung möglicherweise nicht erwünscht ist. Denken Sie daran, ich habe nicht gefragt, warum die Funktion dies getan hat, sondern warum sie im Allgemeinen nicht erwünscht ist, und die Funktion als Beispiel aus der Praxis angegeben.
JustSid
34

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 Stapel legt, scheint es für die Tail-Call-Rekursionsoptimierung möglich zu sein, Code zu erzeugen, der in eine Endlosschleife eintritt, im Gegensatz zu dem nicht optimierten Code, der die Rücksprungadresse auf den Stapel legt die im Falle eines Missbrauchs schließlich überlaufen würde.

Der einzige andere Gedanke, den ich habe, bezieht sich auf das Beibehalten der Aufrufe auf dem Stapel zum Debuggen und Stacktrace-Drucken.

Andrew White
quelle
8
Ich denke, die Erklärung zu Stacktrace / Debugging ist viel wahrscheinlicher (und ich wollte sie veröffentlichen). 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
3
@ughoavgfhw: Vielleicht, aber wenn man sich mit Threading und so beschäftigt, sind Endlosschleifen wirklich schwer zu finden. Ich war immer der Meinung, dass Missbrauch eine Ausnahme auslösen sollte. Da ich das noch nie machen musste, ist es immer noch nur eine Vermutung.
Andrew White
1
Synchronizität, irgendwie ... Ich bin gerade auf einen schlechten 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 ich versucht hätte, "den Heap" (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 töten (da X bald anfing zu werden) reagiert nicht). Vielleicht wäre es ein Grund, den "Fail Fast" -Ansatz zu bevorzugen, der mit einem Stapelüberlauf und keiner Optimierung einhergehen könnte ...? oder vielleicht ist es nur eine andere Sache ...!
ShinTakezou
2
@AndrewWhite Hmm Ich liebe Endlosschleifen total - ich kann mir keine einzige Sache vorstellen, die einfacher zu debuggen ist. Ich meine, Sie können einfach Ihren Debugger anhängen und die genaue Position und den Status des Problems ermitteln, ohne zu raten. Aber wenn Sie Stacktraces von Benutzern erhalten möchten, stimme ich zu, dass eine Endlosschleife problematisch ist, so dass dies logisch erscheint - ein Fehler wird in Ihrem Protokoll angezeigt, eine Endlosschleife nicht.
Voo
1
Dies setzt voraus, dass die Funktion in erster Linie rekursiv ist - ist es aber nicht; weder direkt noch (durch Betrachtung des Kontexts, aus dem die Funktion stammt) indirekt. Ich habe anfangs die gleiche falsche Annahme gemacht.
Konrad Rudolph
21

Ein möglicher Grund besteht darin, das Debuggen und Profilieren zu vereinfachen (mit TCO verschwindet der übergeordnete Stapelrahmen, wodurch es schwieriger wird, Stapelspuren zu verstehen.)

NPE
quelle
2
Es ist allerdings etwas seltsam, die Profilerstellung auf Kosten der 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
Matthieu M.
1
@MatthieuM.: So etwas wäre nicht sinnvoll, 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 und zu belassen 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.
Supercat
@MatthieuM. Wenn das Verdünnen Ihres Öls die Voraussetzung für eine Messung ist, ist dies tatsächlich sinnvoll.
Dmitry Grigoryev
@DmitryGrigoryev: Nein, das tut es nicht. Keine Maßnahme ist ärgerlich, aber eine falsche Maßnahme ist nutzlos bis gefährlich (je nachdem, wie viel Vertrauen Sie in sie setzen). Fahren Sie mit der Ölanalogie fort: Wenn sie Sie verlangsamt, erhalten Sie möglicherweise Maßnahmen, die darauf hinweisen, dass das Gewicht wichtiger ist als die Aerodynamik. Entfernen Sie daher das Gewicht und verschlechtern Sie die Aerodynamik, um das zu optimieren, was Sie gemessen haben. Wenn Sie schneller fahren, stellt sich heraus, dass die Aerodynamik wichtiger war und Ihre "Verbesserung" schlimmer ist als nichts zu tun!
Matthieu M.
@MatthieuM. Kennen Sie das Unsicherheitsprinzip? 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, ändert die Instrumentierung des Autos die Aerodynamik trotzdem.
Dmitry Grigoryev