Implementiert Swift die Tail-Call-Optimierung? und im Fall der gegenseitigen Rekursion?

70

Insbesondere wenn ich folgenden Code habe:

func sum(n: Int, acc: Int) -> Int {
  if n == 0 { return acc }
  else { return sum(n - 1, acc + n) }
}

Wird der Swift-Compiler es zu einer Schleife optimieren? Und in einem interessanteren Fall weiter unten?

func isOdd(n: Int) -> Bool {
  if n == 0 { return false; }
  else { return isEven(n - 1) }
}

func isEven(n: Int) -> Bool {
  if n == 0 { return true }
  else { return isOdd(n - 1) }
}
Alfa07
quelle
2
Der Stapel ist nur so groß. Was passiert, wenn Sie eine unendlich rekursive Funktion ausführen? Stürzt es ab?
Veedrac
@ Veedrac: Es ist Apfel. Es wird in eine Schleife konvertiert und erhält ein deterministisches Ergebnis zurück.
Mare Infinitus
5
@Veedrac - das ist selbstverständlich. Ein funktionaler Programmierer, der eine unendliche Rekursion ausführt, wäre jedoch wie ein zwingender Programmierer, der eine forSchleife ohne Testklausel ausführt, z for (int i = 0; ; i++) { println("%d", i); }.
Yawar
5
@Veedrac Mein Punkt ist, dass ein funktionierender Programmierer nicht mit größerer Wahrscheinlichkeit eine Endlosrekursion ausführt als ein Imperativprogrammierer eine Endlosschleife.
Yawar
2
In Bezug auf die eigenständige Frage "Implementiert Swift die Tail-Call-Optimierung?" Die kurze Antwort lautet: "Swift garantiert keine Tail-Call-Optimierung, verlassen Sie sich also nicht darauf." Wenn Sie jedoch eine Rekursion durchführen, können Sie auch TCO versuchen, da der Compiler Sie möglicherweise unterstützt;)
arcseldon

Antworten:

75

Am besten überprüfen Sie den vom Compiler generierten Assembler-Code. Ich habe den obigen Code genommen und ihn kompiliert mit:

swift -O3 -S tco.swift >tco.asm

Der relevante Teil der Ausgabe

.globl    __TF3tco3sumFTSiSi_Si
    .align    4, 0x90
__TF3tco3sumFTSiSi_Si:
    pushq    %rbp
    movq    %rsp, %rbp
    testq    %rdi, %rdi
    je    LBB0_4
    .align    4, 0x90
LBB0_1:
    movq    %rdi, %rax
    decq    %rax
    jo    LBB0_5
    addq    %rdi, %rsi
    jo    LBB0_5
    testq    %rax, %rax
    movq    %rax, %rdi
    jne    LBB0_1
LBB0_4:
    movq    %rsi, %rax
    popq    %rbp
    retq
LBB0_5:
    ud2

    .globl    __TF3tco5isOddFSiSb
    .align    4, 0x90
__TF3tco5isOddFSiSb:
    pushq    %rbp
    movq    %rsp, %rbp
    testq    %rdi, %rdi
    je    LBB1_1
    decq    %rdi
    jo    LBB1_9
    movb    $1, %al
LBB1_5:
    testq    %rdi, %rdi
    je    LBB1_2
    decq    %rdi
    jo    LBB1_9
    testq    %rdi, %rdi
    je    LBB1_1
    decq    %rdi
    jno    LBB1_5
LBB1_9:
    ud2
LBB1_1:
    xorl    %eax, %eax
LBB1_2:
    popq    %rbp
    retq

    .globl    __TF3tco6isEvenFSiSb
    .align    4, 0x90
__TF3tco6isEvenFSiSb:
    pushq    %rbp
    movq    %rsp, %rbp
    movb    $1, %al
LBB2_1:
    testq    %rdi, %rdi
    je    LBB2_5
    decq    %rdi
    jo    LBB2_7
    testq    %rdi, %rdi
    je    LBB2_4
    decq    %rdi
    jno    LBB2_1
LBB2_7:
    ud2
LBB2_4:
    xorl    %eax, %eax
LBB2_5:
    popq    %rbp
    retq

Der generierte Code enthält keine Aufrufanweisungen, nur bedingte Sprünge ( je/ jne/ jo/ jno). Dies deutet eindeutig darauf hin, dass Swift in beiden Fällen Tail-Call-Optimierungen durchführt .

Darüber hinaus sind die isOdd/ isEven-Funktionen insofern interessant, als der Compiler nicht nur TCO auszuführen scheint, sondern auch jeweils die andere Funktion inline.

Ferruccio
quelle
2
Oh, dein Wort " klar " lässt mich wie eine totale Attrappe fühlen. Aber danke für die Untersuchung - ich bin sicher, es ist ziemlich offensichtlich, wenn Sie ASM kennen.
Skywinder
2
@skywinder - Entschuldigung. Was ich meinte war, dass es keine callAnweisungen im generierten Code gibt, nur bedingte Sprünge ( je/ jne/ jo/ jno)
Ferruccio
Sehr geschätzt!
Skywinder
Sie müssen ASM nicht wirklich kennen, um Ferruccios Analyse zu verstehen. Ein Verständnis der Grundlagen der Verwendung von Stapeln ist ausreichend. Eine Aufrufanweisung ist ein Unterprogramm- / Funktions- / Methodenaufruf. Diese schieben die Rücksprungadresse (und alle Parameter) auf den Stapel. Sprunganweisungen schieben nichts auf den Stapel und können daher nicht zu möglichen Stapelüberläufen führen.
Duncan C
23

Ja, der schnelle Compiler führt in einigen Fällen eine Tail-Call-Optimierung durch:

func sum(n: Int, acc: Int) -> Int {
    if n == 0 { return acc }
    else { return sum(n - 1, acc: acc + 1) }
}

Als globale Funktion wird hierdurch ein konstanter Stapelspeicher auf der Optimierungsstufe "Schnellste" ( -O) verwendet.

Wenn es sich innerhalb einer Struktur befindet, wird immer noch konstanter Stapelspeicher verwendet. Innerhalb einer Klasse führt der Compiler jedoch kein tco aus, da die Methode zur Laufzeit möglicherweise überschrieben wird.

Clang unterstützt auch tco für Objective-C, aber häufig ARC-Aufrufe releasenach dem rekursiven Aufruf, wodurch diese Optimierung verhindert wird (siehe diesen Artikel von Jonathon Mah) Weitere Informationen finden .

ARC scheint auch TCO in Swift zu verhindern:

func sum(n: Int, acc: Int, s: String?) -> Int {
    if n == 0 { return acc }
    else { return sum(n - 1, acc + 1, s) }
}

In meinen Tests wurden keine TCO durchgeführt.

Sebastian
quelle
Meinen Sie damit, dass die gleichen TCO-Vorbehalte, die Jonathon Mah für Obj-C beschreibt, für den Swift-Compiler in der aktuellen Version (1.0) gelten?
Palimondo
@Palimondo Leider sieht es für mich so aus.
Sebastian
5
Ich würde nicht sagen, dass ARC TCO verhindert, sondern dass der rekursive Aufruf nicht mehr in der Endposition ist, da ARC einen Freigabeaufruf hinzufügen muss, bevor er von einer Funktion zurückkehrt.
Ferruccio