我已经将命令式行计数代码(见linesGt1
)从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
}
在执行性能分析或计时时,可以使用Process.range
生成输入,以将实际计算与 I/O 隔离开来。
time { Process.range(0,100000).drop(40000).once.as(true).runLastOr(false).run }
当我第一次运行这个时,在我的机器上花了大约 2.2 秒,这似乎与您所看到的一致。经过几次运行,可能是在 JIT 之后,我一直在 .64 秒左右,原则上,我看不出任何理由为什么即使使用 I/O 也不能那么快(请参阅下面的讨论)。
在我的非正式测试中,scalaz-stream 的每个"步骤"的开销似乎约为 1-2 微秒(例如,尝试Process.range(0,10000)
.如果管道包含多个阶段,则整个流的每个步骤将包含其他几个步骤。考虑最小化scalaz-stream开销的方法只是为了确保您在每一步都做了足够的工作,以使scalaz-stream本身增加的任何开销相形见绌。这篇文章有关于这种方法的更多详细信息。行计数示例是一种最糟糕的情况,因为您几乎不做每一步的工作,只是在计算步骤。
因此,我会尝试编写一个每步读取多行的linesR
版本,并确保在 JIT 后进行测量。