Swift是否实现尾部调用优化?在相互递归的情况下?


70

特别是如果我有以下代码:

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

Swift编译器会将其优化为循环吗?并在下面更有趣的情况下这样做吗?

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) }
}

2
堆栈只有这么大。当您运行无限递归函数时会发生什么?它会崩溃吗?
Veedrac 2014年

@Veedrac:是苹果。它将转换为循环并返回确定性结果。
Mare Infinitus 2014年

5
@Veedrac-这是给定的。但是,执行无限递归的函数式程序员就像命令式程序员在for没有测试子句的情况下进行循环一样,例如for (int i = 0; ; i++) { println("%d", i); }
Yawar

5
@Veedrac我的观点是,功能性程序员比命令式程序员进行无限循环更不可能进行无限递归。
Yawar'1

2
关于独立问题“ Swift是否实现了尾部调用优化?” 简短的答案是“ Swift不能保证尾部呼叫优化,因此不要依赖它”。但是,如果您要进行递归操作,则不妨尝试TCO,因为编译器可能会提供帮助;)
arcseldon 2015年

Answers:


75

最好的检查方法是检查编译器生成的汇编语言代码。我将上面的代码编译为:

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

输出的相关部分

.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

生成的代码中没有任何呼叫说明,只有条件跳转(je/ jne/ jo/ jno)。显然,这表明Swift确实在两种情况下都进行了尾部调用优化。

此外,isOdd/isEven函数很有趣,因为编译器不仅似乎在执行TCO,而且还在每种情况下内联其他函数。


2
哦,您的“明晰”一词让我觉得自己像个傻瓜。但是感谢您的调查-如果您知道ASM,我敢肯定这很明显。
skywinder

2
@skywinder-对此感到抱歉。我的意思是,call生成的代码中没有指令,只有条件跳转(je/ jne/ jo/ jno
Ferruccio

非常感激!
skywinder

您不必真正了解ASM就可以了解Ferruccio的分析。了解如何使用堆栈的基础知识就足够了。调用指令是子例程/函数/方法的调用。那些将返回地址(和任何参数)压入堆栈。跳转指令不会将任何内容压入堆栈,因此它们不会导致潜在的堆栈溢出。
Duncan C

23

是的,在某些情况下,swift编译器会执行尾部调用优化:

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

作为全局函数,它将在“最快”优化级别(-O)上使用恒定的堆栈空间。

如果它在结构内部,它将仍然使用恒定的堆栈空间。但是,在类内,编译器不会执行tco,因为该方法可能在运行时被覆盖。

Clang还支持Objective-C的tco,但release在递归调用之后经常会进行ARC调用,从而阻止了这种优化,有关更多详细信息,请参阅Jonathon Mah的本文

ARC似乎也阻止了Swift中的TCO:

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

我的测试没有执行TCO。


您是说Jonathon Mah为Obj-C描述的相同的TCO警告适用于当前(1.0)版本的Swift编译器吗?
Palimondo 2014年

@Palimondo不幸的是,那就是我的样子。
塞巴斯蒂安

5
我并不是说ARC可以防止TCO,而是因为ARC必须在从函数返回之前添加释放调用,因此递归调用不再位于尾部。
Ferruccio 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.