Welche C ++ - Compiler optimieren gegebenenfalls die Tail-Rekursion?

149

Es scheint mir, dass es perfekt funktionieren würde, eine Tail-Rekursionsoptimierung sowohl in C als auch in C ++ durchzuführen, aber beim Debuggen sehe ich nie einen Frame-Stack, der diese Optimierung anzeigt. Das ist irgendwie gut, denn der Stapel sagt mir, wie tief die Rekursion ist. Die Optimierung wäre aber auch nett.

Führen C ++ - Compiler diese Optimierung durch? Warum? Warum nicht?

Wie kann ich dem Compiler sagen, dass er es tun soll?

  • Für MSVC: /O2oder/Ox
  • Für GCC: -O2oder-O3

Wie wäre es zu überprüfen, ob der Compiler dies in einem bestimmten Fall getan hat?

  • Aktivieren Sie für MSVC die PDB-Ausgabe, um den Code verfolgen zu können, und überprüfen Sie dann den Code
  • Für GCC ..?

Ich würde immer noch Vorschläge machen, wie ich feststellen kann, ob eine bestimmte Funktion vom Compiler so optimiert wird (obwohl ich es beruhigend finde, dass Konrad mir sagt, ich solle sie annehmen).

Es ist immer möglich zu überprüfen, ob der Compiler dies überhaupt tut, indem er eine Endlosrekursion durchführt und prüft, ob dies zu einer Endlosschleife oder einem Stapelüberlauf führt (ich habe dies mit GCC gemacht und herausgefunden, dass dies -O2ausreichend ist), aber ich möchte es sein in der Lage, eine bestimmte Funktion zu überprüfen, von der ich weiß, dass sie trotzdem beendet wird. Ich würde gerne eine einfache Möglichkeit haben, dies zu überprüfen :)


Nach einigen Tests stellte ich fest, dass Destruktoren die Möglichkeit dieser Optimierung ruinieren. Manchmal kann es sich lohnen, den Gültigkeitsbereich bestimmter Variablen und Provisorien zu ändern, um sicherzustellen, dass sie außerhalb des Gültigkeitsbereichs liegen, bevor die return-Anweisung beginnt.

Wenn nach dem Tail-Call ein Destruktor ausgeführt werden muss, kann die Tail-Call-Optimierung nicht durchgeführt werden.

Magnus Hoff
quelle

Antworten:

128

Alle aktuellen Mainstream-Compiler führen die Tail-Call-Optimierung ziemlich gut durch (und dies seit mehr als einem Jahrzehnt), selbst für gegenseitig rekursive Aufrufe wie:

int bar(int, int);

int foo(int n, int acc) {
    return (n == 0) ? acc : bar(n - 1, acc + 2);
}

int bar(int n, int acc) {
    return (n == 0) ? acc : foo(n - 1, acc + 1);
}

Es ist unkompliziert, den Compiler die Optimierung durchführen zu lassen: Schalten Sie einfach die Optimierung für Geschwindigkeit ein:

  • Verwenden Sie für MSVC /O2oder /Ox.
  • Verwenden Sie für GCC, Clang und ICC -O3

Eine einfache Möglichkeit, um zu überprüfen, ob der Compiler die Optimierung durchgeführt hat, besteht darin, einen Aufruf auszuführen, der andernfalls zu einem Stapelüberlauf führen würde - oder die Assembly-Ausgabe zu überprüfen.

Als interessante historische Anmerkung wurde dem GCC im Rahmen einer Diplomarbeit von Mark Probst die Tail-Call-Optimierung für C hinzugefügt . Die Arbeit beschreibt einige interessante Vorbehalte bei der Implementierung. Es lohnt sich zu lesen.

Konrad Rudolph
quelle
ICC würde das tun, glaube ich. Nach meinem besten Wissen produziert ICC den schnellsten Code auf dem Markt.
Paul Nathan
35
@Paul Die Frage ist, wie viel von der Geschwindigkeit des ICC-Codes durch algorithmische Optimierungen wie Tail-Call-Optimierungen verursacht wird und wie viel durch die Cache- und Mikrobefehlsoptimierungen verursacht wird, die nur Intel mit seinem genauen Wissen über die eigenen Prozessoren leisten kann.
Imagist
6
gcchat eine engere Option, -foptimize-sibling-callsum "rekursive Geschwister- und Schwanzaufrufe zu optimieren". Diese Option (nach gcc(1)Handbuchseite für die Versionen 4.4, 4.7 und 4.8 Targeting verschiedene Plattformen) auf Ebenen aktiviert -O2, -O3, -Os.
FooF
Wenn Sie im DEBUG-Modus ausführen, ohne explizit Optimierungen anzufordern, wird KEINE Optimierung durchgeführt. Sie können PDB für den True Release-Modus EXE aktivieren und versuchen, dies zu durchlaufen. Beachten Sie jedoch, dass das Debuggen im Release-Modus Komplikationen hat - unsichtbare / entfernte Variablen, zusammengeführte Variablen, Variablen, die im unbekannten / unerwarteten Bereich den Gültigkeitsbereich verlassen, Variablen, die niemals eingehen Umfang und wurden wahre Konstanten mit Adressen auf Stapelebene und - nun ja - zusammengeführten oder fehlenden Stapelrahmen. Normalerweise bedeuten zusammengeführte Stapelrahmen, dass Angerufene inline sind, und fehlende / rückgängig gemachte Rahmen wahrscheinlich einen Endanruf.
7етър Петров
21

gcc 4.3.2 fügt diese Funktion (beschissene / triviale atoi()Implementierung) vollständig ein main(). Optimierungsstufe ist -O1. Ich merke, wenn ich damit herumspiele (selbst wenn ich es von staticauf externändere, verschwindet die Schwanzrekursion ziemlich schnell, sodass ich mich bei der Programmkorrektheit nicht darauf verlassen würde.

#include <stdio.h>
static int atoi(const char *str, int n)
{
    if (str == 0 || *str == 0)
        return n;
    return atoi(str+1, n*10 + *str-'0');
}
int main(int argc, char **argv)
{
    for (int i = 1; i != argc; ++i)
        printf("%s -> %d\n", argv[i], atoi(argv[i], 0));
    return 0;
}
Tom Barta
quelle
1
Sie können jedoch die Optimierung der Verbindungszeit aktivieren, und ich denke, dass dann sogar eine externMethode inline sein könnte.
Konrad Rudolph
5
Seltsam. Ich habe gcc 4.2.3 (x86, Slackware 12.1) getestet und gcc 4.6.2 (AMD64, Debian sid) und mit-O1 gibt es kein inlining und keine Schwanz-Rekursion Optimierung . Sie müssen verwenden -O2dafür (na ja, in 4.2.x, die nun eher alte ist, wird es noch nicht inline sein). Übrigens: Es ist auch erwähnenswert, dass gcc die Rekursion optimieren kann, auch wenn es sich nicht ausschließlich um eine Endrekursion handelt (wie bei einem faktoriellen Akku ohne Akkumulator).
Przemoc
16

Neben dem Offensichtlichen (Compiler führen diese Art der Optimierung nur durch, wenn Sie danach fragen) ist die Tail-Call-Optimierung in C ++ komplex: Destruktoren.

Gegeben etwas wie:

   int fn(int j, int i)
   {
      if (i <= 0) return j;
      Funky cls(j,i);
      return fn(j, i-1);
   }

Der Compiler kann dies (im Allgemeinen) nicht durch Tail-Call optimieren, da er den Destruktor von aufrufen muss, cls nachdem der rekursive Aufruf zurückgegeben wurde.

Manchmal kann der Compiler feststellen, dass der Destruktor keine von außen sichtbaren Nebenwirkungen hat (dies kann also frühzeitig erfolgen), aber oft nicht.

Eine besonders häufige Form davon ist, wo Funkytatsächlich eine std::vectoroder eine ähnliche ist.

Martin Bonner unterstützt Monica
quelle
Funktioniert bei mir nicht Das System teilt mir mit, dass meine Stimme gesperrt ist, bis die Antwort bearbeitet wird.
Hmuelner
Ich habe gerade die Antwort bearbeitet (Klammern entfernt) und jetzt konnte ich meine Ablehnung rückgängig machen.
Hmuelner
11

Die meisten Compiler führen in einem Debug-Build keine Optimierung durch.

Wenn Sie VC verwenden, versuchen Sie es mit einem Release-Build mit aktivierten PDB-Informationen. Auf diese Weise können Sie die optimierte App nachverfolgen und sollten hoffentlich sehen, was Sie dann möchten. Beachten Sie jedoch, dass Sie durch das Debuggen und Verfolgen eines optimierten Builds überall herumspringen und Variablen häufig nicht direkt untersuchen können, da sie immer nur in Registern landen oder vollständig optimiert werden. Es ist eine "interessante" Erfahrung ...

Greg Whitfield
quelle
2
Versuchen Sie gcc why -g -O3 und erhalten Sie Optimierungen in einem Debug-Build. xlC hat das gleiche Verhalten.
G24l
Wenn Sie "die meisten Compiler" sagen: Welche Sammlungen von Compilern betrachten Sie? Wie bereits erwähnt, gibt es mindestens zwei Compiler, die während des Debug-Builds Optimierungen durchführen - und soweit ich weiß, tut VC dies auch (außer wenn Sie das Ändern und Fortfahren möglicherweise aktivieren).
Skyking
7

Wie Greg erwähnt, werden Compiler dies im Debug-Modus nicht tun. Es ist in Ordnung, dass Debug-Builds langsamer sind als ein Prod-Build, aber sie sollten nicht öfter abstürzen. Wenn Sie auf eine Tail-Call-Optimierung angewiesen sind, können sie genau das tun. Aus diesem Grund ist es oft am besten, den Tail-Aufruf als normale Schleife umzuschreiben. :-(

0124816
quelle