【问题标题】:Could not optimize methods无法优化方法
【发布时间】:2017-10-04 20:41:29
【问题描述】:

这是引发编译错误“递归调用不在尾部位置”的最小代码。但是,我使用的是@inline,递归调用 is 在尾部位置。我使用这个@inline 的原因是我将原始reccall 的代码复制了两次。

import scala.annotation._
object Test {
  @tailrec private def test(i: Int): Int = {
    @inline def reccall(i: Int): Int = test(i-1)
    i match {
      case 0 => 0
      case i => reccall(i)
    }
  }
}

我查看了Recursive call not in tail position@tailrec why does this method not compile with 'contains a recursive call not in tail position'? 的答案,但它们不适用于我的情况。使用 Scala 2.12

【问题讨论】:

标签: scala inline tail-recursion


【解决方案1】:

看来,@inline 的实现方式还是通过栈传参。通过插入内联代码消除了跳转,但堆栈仍用于参数。这使得它不可能处于尾部位置,因为调用完成后需要清理堆栈。

此外,使用@inline 注释函数并不能保证优化器将内联它,只是它会“特别努力地尝试”。

【讨论】:

  • 我非常喜欢你的回答。它强调提问者希望@inline 会消除额外的堆栈帧。
  • 您是否有任何参考证明@inline 实际上仍在将参数推入堆栈,而不是像我期望的那样推入局部变量?
  • 做,我不做 :) 我只是在猜测,它可能会这样做。
【解决方案2】:

嗯,尾递归在JVM中是如何实现的机制解释如下:

Scala,在尾递归的情况下,可以消除创建一个 新的堆栈框架,只需重新使用当前的堆栈框架。堆栈 无论递归调用多少次,都不会变得更深 制作。

因此,在您的情况下,它不能重用属于 test 方法的当前堆栈帧,因为它必须为 reccall 方法创建一个新的堆栈帧。

在这种情况下,递归调用是隐式的,由另一种方法进行。所以我相信你不能真正为这种情况实施尾递归。

你可以干脆把reccall方法去掉,写case i => test(i-1),编译器就不会报错了。

注意:我也相信@inline 与此无关,在此示例中不是必需的,因为如果我删除它 - 编译器仍然会抱怨同样的原因。

【讨论】:

  • reccall 的调用不应该手动内联,因为它在我的真实示例中包含大量代码。请理解这是一个简化的代码,而不是我的完整代码。
  • @MikaëlMayer,我现在明白了。我最初并没有想到您认为@inline 将消除该堆栈帧并通过局部变量推送 args。现在我明白了。
【解决方案3】:

这里的问题是@inline 是严格建议的:它不保证编译器会内联函数。由于@tailrec 只有在绝对保证可以消除尾调用的情况下才有效,这意味着使用@tailrec 必须假设没有内联。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-03-26
    • 2021-05-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-11-04
    • 2012-05-07
    相关资源
    最近更新 更多