【问题标题】:Why is headOption faster为什么 headOption 更快
【发布时间】:2013-07-31 10:24:28
【问题描述】:

我对一些代码进行了更改,它的速度提高了 4.5 倍。我想知道为什么。它曾经本质上是:

def doThing(queue: Queue[(String, String)]): Queue[(String, String)] = queue match {
  case Queue((thing, stuff), _*) => doThing(queue.tail)
  case _ => queue
}

我把它改成这样以获得巨大的速度提升:

def doThing(queue: Queue[(String, String)]): Queue[(String, String)] = queue.headOption match {
  case Some((thing, stuff)) => doThing(queue.tail)
  case _ => queue
}

_* 有什么作用,为什么它比 headOption 贵?

【问题讨论】:

  • 这是可变的Queue 还是不可变的?
  • 你的编译器版本是多少?
  • Scala 版本是 2.10.2

标签: scala optimization queue option


【解决方案1】:

_* 用于指定可变参数,因此您在第一个版本中所做的是将 Queue 解构为一对字符串,以及适当数量的其他字符串对 - 即您正在解构整个 Queue虽然你只关心第一个元素。

如果你只是去掉星号,给

def doThing(queue: Queue[(String, String)]): Queue[(String, String)] = queue match {
  case Queue((thing, stuff), _) => doThing(queue.tail)
  case _ => queue
}

那么您只是将队列解构为一对字符串和一个余数(因此不需要完全解构)。这应该在与您的第二个版本相当的时间内运行(但我自己还没有计时)。

【讨论】:

  • Queue((thing, stuff), _)2 元素集合的模式。
  • @senia 哎呀,你是对的 - 我在想队列是像列表一样构造的。尽管如此,关于为什么 _* 需要这么长时间的解构论点仍然适用(但坚持使用 headOption 或 dequeue 或类似方法)。
  • Queue 实际上是使用 2 个列表构造的。但List(a, b) 也是 2 个元素列表的模式。
【解决方案2】:

您的代码示例之间应该没有显着差异。

case Queue((thing, stuff), _*)实际上被编译器翻译成调用head(apply(0))方法。您可以使用scalac -Xprint:patmat 对此进行调查:

<synthetic> val p2: (String, String) = o9.get.apply(0);
if (p2.ne(null))
  matchEnd6(doThing(queue.tail))

head的费用和headOption的费用差不多。

方法headtaildequeue 可能会在Queue 的内部List 上导致reverce(成本O(n))。在您的两个代码示例中,最多会有 2 个 reverce 调用。 您应该像这样使用dequeue 最多获得一个reverce 调用:

def doThing(queue: Queue[(String, String)]): Queue[(String, String)] =
  if (queue.isEmpty) queue
  else queue.dequeue match {
    case (e, q) => doThing(q)
  }

您也可以将(thing, stuff) 替换为_。在这种情况下,编译器将只生成lengthCompare 的调用,而没有headtail

if (o9.get != null && o9.get.lengthCompare(1) >= 0)

【讨论】:

  • 谢谢!我也会试试的。但为什么我的示例中存在差异?
  • @kelloti:你确定这两个例子的性能不一样吗?您的代码可以通过 jit 优化为无用。正如我从scalac -Xprint:patmat 看到的那样,您的两个代码示例之间没有显着差异。
  • @senia,我不认为 headtail 是 O(n)。见github.com/scala/scala/blob/v2.10.2/src/library/scala/…。它确实利用了结构共享,对我来说似乎 O(1)。对不起,我把 O(1) 拿回来了,里面有一个in.reverse。但dequeue 也是如此。
  • 你是对的,它是O(1)。我可以看到唯一的选择:您的基准测试不正确。
  • 您的示例中最多有 2 个in.reversedequeue 最多有 1 个。这不是显着的性能差异。
【解决方案3】:

我在使用-Xprint:all 运行 scalac 后的猜测是,在 queue match { case Queue((thing, stuff), _*) =&gt; doThing(queue.tail) } 示例中 patmat 的末尾,我看到调用了以下方法(为简洁起见进行了编辑):

val o9 = scala.collection.immutable.Queue.unapplySeq[(String, String)](x1);
if (o9.isEmpty.unary_!)
  if (o9.get.!=(null).&&(o9.get.lengthCompare(1).>=(0)))
    {
      val p2: (String, String) = o9.get.apply(0);
      val p3: Seq[(String, String)] = o9.get.drop(1);

所以lengthCompare 以一种可能优化的方式比较集合的长度。对于Queue,它会创建一个迭代器并迭代一次。所以这应该有点快。另一方面,drop(1) 也构造了一个迭代器,跳过一个元素并将其余元素添加到结果队列中,这样集合的大小将是线性的。

headOption 示例更简单,它检查列表是否为空(两次比较),如果不是则返回 Some(head),然后将其 _1_2 分配给 thingstuff。所以没有迭代器被创建,集合的长度也没有任何线性关系。

【讨论】:

  • 你的编译器版本是多少?我正在使用2.10.12.11.0-M3,但没有p3
  • @senia,2.10.0。你在检查 immutable 队列吗?
  • @senia,我明白你的意思,我确实看到 o9.get.apply(0) 有更新的版本,而没有 drop
  • pastebin。我想这是版本的改进>2.10.0.
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-12-25
  • 2013-05-29
  • 2011-05-01
  • 2013-03-21
  • 2015-10-24
  • 2020-06-02
相关资源
最近更新 更多