使用scalaz-stream进行行计数的性能

3
我已经翻译了命令式行计数代码(请参见linesGt1),从Functional Programming in Scala第15章的开头到使用scalaz-stream(请参见linesGt2)的解决方案。然而,linesGt2的性能并不是很好。命令式代码比我的scalaz-stream解决方案快约30倍。所以我想我做错了什么基本的事情。如何提高scalaz-stream代码的性能?
这是我的完整测试代码:
import scalaz.concurrent.Task
import scalaz.stream._

object Test06 {

val minLines = 400000

def linesGt1(filename: String): Boolean = {
  val src = scala.io.Source.fromFile(filename)
  try {
    var count = 0
    val lines: Iterator[String] = src.getLines
    while (count <= minLines && lines.hasNext) {
      lines.next
      count += 1
    }
    count > minLines
  }
  finally src.close
}

def linesGt2(filename: String): Boolean =
  scalaz.stream.io.linesR(filename)
    .drop(minLines)
    .once
    .as(true)
    .runLastOr(false)
    .run

def time[R](block: => R): R = {
  val t0 = System.nanoTime()
  val result = block
  val t1 = System.nanoTime()
  println("Elapsed time: " + (t1 - t0) / 1e9 + "s")
  result
}

time(linesGt1("/home/frank/test.txt"))        //> Elapsed time: 0.153122057s
                                              //| res0: Boolean = true
time(linesGt2("/home/frank/test.txt"))        //> Elapsed time: 4.738644606s
                                              //| res1: Boolean = true
}

我不是scalaz-stream的专家,但这对我来说看起来很合理(尽管我可能会使用类似.once.runLast.run.nonEmpty的东西)。使用drop逐行步进需要很多开销,我猜这就是你在这里看到的。 - Travis Brown
1个回答

2
当您进行分析或计时时,您可以使用Process.range生成输入以将实际计算与I/O隔离开来。 改编您的示例:
time { Process.range(0,100000).drop(40000).once.as(true).runLastOr(false).run }

当我第一次运行它时,在我的机器上大约需要2.2秒,这似乎与您看到的结果一致。几次运行后,可能是JIT编译后,我始终能够得到大约0.64秒的结果,并且原则上,即使有I/O,我也看不到为什么不能同样快(请参见下面的讨论)。
在我的非正式测试中,每个“步骤”的scalaz-stream开销似乎约为1-2微秒(例如,请尝试Process.range(0,10000))。如果你有一个包含多个阶段的管道,则整个流的每个步骤将由几个其他步骤组成。考虑最小化scalaz-stream开销的方法就是确保在每个步骤中做足够的工作,以使scalaz-stream本身增加的任何开销变得微不足道。 此帖子有更多关于这种方法的细节。行计数示例是最糟糕的情况,因为你几乎没有在每个步骤中做任何工作,只是计算步骤数。
所以我建议尝试编写一个版本的linesR,每个步骤读取多行,并确保在JIT编译之后进行测量。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接