为什么尾递归在这段代码中没有带来更好的性能?

发布于 2024-11-27 00:25:31 字数 1068 浏览 0 评论 0原文

我正在创建一种更快的字符串分割器方法。首先,我编写了一个返回 List 的非尾递归版本。接下来,使用 ListBuffer 进行尾递归,然后调用 toList+=toList 的时间复杂度为 O(1) )。我完全期望尾递归版本更快,但事实并非如此。

谁能解释为什么?

原始版本:

def split(s: String, c: Char, i: Int = 0): List[String] = if (i < 0) Nil else {
  val p = s indexOf (c, i)
  if (p < 0) s.substring(i) :: Nil else s.substring(i, p) :: split(s, c, p + 1)
}

尾递归一:

import scala.annotation.tailrec
import scala.collection.mutable.ListBuffer
def split(s: String, c: Char): Seq[String] = {
  val buffer = ListBuffer.empty[String]
  @tailrec def recurse(i: Int): Seq[String] =  {
    val p = s indexOf (c, i)
    if (p < 0) {
      buffer += s.substring(i)
      buffer.toList
    } else {
      buffer += s.substring(i, p)
      recurse(p + 1)
    }
  }
  recurse(0)
}

使用代码此处进行了基准测试,结果此处,作者:#scala 的 jyxent。

I was creating a faster string splitter method. First, I wrote a non-tail recursive version returning List. Next, a tail recursive one using ListBuffer and then calling toList (+= and toList are O(1)). I fully expected the tail recursive version to be faster, but that is not the case.

Can anyone explain why?

Original version:

def split(s: String, c: Char, i: Int = 0): List[String] = if (i < 0) Nil else {
  val p = s indexOf (c, i)
  if (p < 0) s.substring(i) :: Nil else s.substring(i, p) :: split(s, c, p + 1)
}

Tail recursive one:

import scala.annotation.tailrec
import scala.collection.mutable.ListBuffer
def split(s: String, c: Char): Seq[String] = {
  val buffer = ListBuffer.empty[String]
  @tailrec def recurse(i: Int): Seq[String] =  {
    val p = s indexOf (c, i)
    if (p < 0) {
      buffer += s.substring(i)
      buffer.toList
    } else {
      buffer += s.substring(i, p)
      recurse(p + 1)
    }
  }
  recurse(0)
}

This was benchmarked with code here, with results here, by #scala's jyxent.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

三五鸿雁 2024-12-04 00:25:31

在第二种情况下,你只是做了更多的工作。在第一种情况下,您可能会溢出堆栈,但每个操作都非常简单,并且 :: 是您能得到的尽可能小的包装器(您所要做的就是创建包装器并指向它到另一个列表的头部)。在第二种情况下,您不仅最初创建一个额外的集合并且必须围绕 sbuffer 形成一个闭包以供嵌套方法使用,而且您还使用较重的 ListBuffer 必须检查每个 += 是否已被复制到列表中,并根据它是否为空使用不同的代码路径(以便得到O(1) 附加到工作)。

You're simply doing more work in the second case. In the first case, you might overflow your stack, but every operation is really simple, and :: is as small of a wrapper as you can get (all you have to do is create the wrapper and point it to the head of the other list). In the second case, not only do you create an extra collection initially and have to form a closure around s and buffer for the nested method to use, but you also use the heavierweight ListBuffer which has to check for each += whether it's already been copied out to a list, and uses different code paths depending on whether it's empty or not (in order to get the O(1) append to work).

月牙弯弯 2024-12-04 00:25:31

由于尾部调用优化,您预计尾部递归版本会更快,如果您将苹果与苹果进行比较,我认为这是正确的:

def split3(s: String, c: Char): Seq[String] = {
  @tailrec def recurse(i: Int, acc: List[String] = Nil): Seq[String] =  {
    val p = s indexOf (c, i)
    if (p < 0) {
      s.substring(i) :: acc
    } else {
      recurse(p + 1, s.substring(i, p) :: acc)
    }
  }
  recurse(0) // would need to reverse
}

我将这个 split3 计时得更快,当然除了获得同样的结果需要颠倒结果。

ListBuffer 似乎确实引入了尾递归优化无法弥补的低效率。

编辑:考虑避免相反的情况...

def split3(s: String, c: Char): Seq[String] = {
  @tailrec def recurse(i: Int, acc: List[String] = Nil): Seq[String] =  {
    val p = s lastIndexOf (c, i)
    if (p < 0) {
      s.substring(0, i + 1) :: acc
    } else {
      recurse(p - 1, s.substring(p + 1, i + 1) :: acc)
    }
  }
  recurse(s.length - 1)
}

这具有尾部调用优化并避免 ListBuffer

You expect the tail recursive version to be faster due to the tail call optimization and I think this is right, if you compare apples to apples:

def split3(s: String, c: Char): Seq[String] = {
  @tailrec def recurse(i: Int, acc: List[String] = Nil): Seq[String] =  {
    val p = s indexOf (c, i)
    if (p < 0) {
      s.substring(i) :: acc
    } else {
      recurse(p + 1, s.substring(i, p) :: acc)
    }
  }
  recurse(0) // would need to reverse
}

I timed this split3 to be faster, except of course to get the same result it would need to reverse the result.

It does seem ListBuffer introduces inefficiencies that the tail recursion optimization cannot make up for.

Edit: thinking about avoiding the reverse...

def split3(s: String, c: Char): Seq[String] = {
  @tailrec def recurse(i: Int, acc: List[String] = Nil): Seq[String] =  {
    val p = s lastIndexOf (c, i)
    if (p < 0) {
      s.substring(0, i + 1) :: acc
    } else {
      recurse(p - 1, s.substring(p + 1, i + 1) :: acc)
    }
  }
  recurse(s.length - 1)
}

This has the tail call optimization and avoids ListBuffer.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文