为什么尾递归在这段代码中没有带来更好的性能?
我正在创建一种更快的字符串分割器方法。首先,我编写了一个返回 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)
}
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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
在第二种情况下,你只是做了更多的工作。在第一种情况下,您可能会溢出堆栈,但每个操作都非常简单,并且
::
是您能得到的尽可能小的包装器(您所要做的就是创建包装器并指向它到另一个列表的头部)。在第二种情况下,您不仅最初创建一个额外的集合并且必须围绕s
和buffer
形成一个闭包以供嵌套方法使用,而且您还使用较重的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 arounds
andbuffer
for the nested method to use, but you also use the heavierweightListBuffer
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 theO(1)
append to work).由于尾部调用优化,您预计尾部递归版本会更快,如果您将苹果与苹果进行比较,我认为这是正确的:
我将这个
split3
计时得更快,当然除了获得同样的结果需要颠倒结果。ListBuffer 似乎确实引入了尾递归优化无法弥补的低效率。
编辑:考虑避免相反的情况...
这具有尾部调用优化并避免
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:
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...
This has the tail call optimization and avoids
ListBuffer
.