【问题标题】:A scala program much slower than the corresponding python script一个 scala 程序比相应的 python 脚本慢得多
【发布时间】:2011-10-24 11:05:36
【问题描述】:

我编写了一个简短的 Scala 程序来读取一个大文件,对其进行处理并将结果存储在另一个文件中。该文件包含大约 60000 行数字,我只需要从第三行中提取第一个数字。最终,我将这些数字保存到另一个文件中。虽然是数字,但我一直把它们当作字符串。

这是 Scala 代码:

import scala.io.Source
import java.io.BufferedWriter
import java.io.FileWriter

object Analyze {
  def main(args: Array[String]) {
      val fname = "input.txt"

      val counters = Source.fromFile(fname).mkString.split("\\n").grouped(3)
        .map(_(2).split("\\s+")(0))

      val f = new BufferedWriter(new FileWriter("output1.txt"))
      f.write(counters.reduceLeft(_ + "\n" + _))
      f.close()
  }
}

我非常喜欢 Scala 强大的 one liner 的能力。上述代码中的 one-liner 从文件中读取整个文本,将其拆分为行,将行分组为 3 行一组,然后从每组中取出第三行,将其拆分并取第一个数字。

这是等效的python脚本:

fname = 'input.txt'

with file(fname) as f:
    lines = f.read().splitlines()
    linegroups = [lines[i:i+3] for i in range(0, len(lines), 3)]
    nums = [linegroup[2].split()[0] for linegroup in linegroups]

with file('output2.txt', 'w') as f:
    f.write('\n'.join(nums))    

Python 不具备这样的一种衬里。在上面的脚本中,第一行代码将文件读入行列表,下一行将行分成 3 组,下一行创建一个列表,其中包含每组最后一行的第一个数字。它与 Scala 代码非常相似,只是运行速度要快得多。

python 脚本在我的笔记本电脑上运行只需几分之一秒,而 Scala 程序运行 15 秒!我注释掉了将结果保存到文件的代码,持续时间下降到 5 秒,这仍然太慢了。我也不明白为什么将数字保存到文件需要这么长时间。当我处理较大的文件时,python 脚本运行了几秒钟,而 Scala 程序的运行时间是几分钟,我不能用它来分析我的文件。

感谢您对此问题的建议。 谢谢

【问题讨论】:

  • 我还没有尝试过,但我觉得mkString.split("\\n") 不可能有效率。试试Source.fromPath("myfile.txt").getLines() 或类似的东西。
  • 您将此代码作为脚本运行(使用scala 命令)还是预编译?我认为你有这个问题,因为 scala 不是短跑运动员,它是长跑运动员(在启动时花费了大量时间 - 启动 JVM,检查类路径等等)。
  • 嗨,我尝试用 getLines 替换 mkString.split("\\n") ,它确实显着提高了速度 - 收集数据只需一秒钟。仍然保存大约需要 5 秒。我发现reduceLeft 在大量数据上占用了剩下的时间,所以我将其转换为简单的for (c <- counters) f.write(c + "\n"),再次,时间显着缩短。整个过程现在大约需要 5 秒。但这仍然比 python 慢。

标签: scala


【解决方案1】:

我冒昧地清理了代码,这应该通过避免初始 mkString 来更有效地运行,不需要正则表达式来执行空格拆分,并且在写出结果之前不预先聚合结果。我还使用了更好的自我记录方法:

val fname = "input.txt"
val lines = (Source fromFile fname).getLines
val counters =
  (lines grouped 3 withPartial false) map { _.last takeWhile (!_.isWhitespace) }

val f = new BufferedWriter(new FileWriter("output1.txt"))
f.write(counters mkString "\n")
f.close()

警告,未经测试的代码

但这在很大程度上无关紧要,具体取决于您的分析方式。如果您在指标中包含 JVM 启动时间,那么所有的赌注都没有了——再多的代码优化也无济于事。

我通常还建议通过在计时之前运行例程几百次来预热 JVM,但这在文件 I/O 方面不太实用。

【讨论】:

  • 我认为最好使用 counters foreach { f.println } 和 PrintWriter f
  • @incrop - 我确实考虑过,但与原来的相比,这会留下额外的尾随 \n... 可能是正确的做法,但我也想避免改变行为。
【解决方案2】:

我对 Kevin 提供的版本进行了少量修改(删除了 withPartial,因为 python 版本也不处理填充):

import scala.io.Source
import java.io.BufferedWriter
import java.io.FileWriter

object A extends App {

  val fname = "input.txt"
  val lines = (Source fromFile fname).getLines
  val counters =
    (lines grouped 3) map { _.last takeWhile (!_.isWhitespace) }

  val f = new BufferedWriter(new FileWriter("output1.txt"))
  f.write(counters mkString "\n")
  f.close()
}

这里有 60,000 行的时间:

$ time scala -cp classes A

real    0m2.823s

$ time /usr/bin/python A.py

real    0m0.437s

有 900,000 行:

$ time scala -cp classes A

real    0m5.226s

$ time /usr/bin/python A.py

real    0m3.319s

有 2,700,000 行:

$ time scala -cp classes A

real    0m9.516s

$ time /usr/bin/python A.py

real    0m10.635s

此后,scala 版本的性能优于 python 版本。所以看起来有些长的时间是由于 JVM 初始化和 JIT 编译时间。

【讨论】:

  • 还有一个有趣的事实:awk 'NR % 3 == 0 { print $1}' input.txt > output.txt 在相同的输入文件上优于 python ans scala。
  • 哦,是的...这绝对是 awk 的工作
  • 非常感谢您花时间进行实验。事实上mkStringreduce 快得多。我试图重复这个实验,但不幸的是得到了不同的结果。对于 900,000 行,python 在 4.2 秒内完成,而 Scala 在 54 秒内完成。现在,创建 counters 的代码在 Scala 中的运行速度比在 python 中更快——0.3 秒对 4 秒。但是将counters 转换为文本行的代码在python 中花费了几分之一秒(使用join 函数),而对于Scala 则花费了大约54 秒。
  • 我还在 60000 行文件上比较了 Jython(python 端口到 JVM),它的运行速度比 Scala 快 - 1.1 秒对 3.4 秒。它们都在JVM上运行,所以现在我更加困惑了..
  • @CodeChords 伙计,您是在 900,000 行还是 Kevin 的(或我的变体)上计时您的原始版本?您使用的是什么 Scala 和 Java 版本?您的设置似乎有问题。作为比较,我在 HotSpot(TM) 客户端 VM 上使用 2.9.0.final,在 Core 2 Duo 1.5GHz 上使用 Java 1.6.0_25,最大堆大小为 256MB。
【解决方案3】:

除了@notan3xit 的回答,您还可以将计数器写入文件而不先连接它们:

  val f = new BufferedWriter(new FileWriter("output1.txt"))
  f.write(counters.head.toString)
  counters.tail.foreach(c => f.write("\n" + c.toString))
  f.close()

虽然你可以在 Python 中做同样的事情......

【讨论】:

  • 嗨,感谢您的建议,我尝试了与此类似的方法,并在@notan3xit 上发表了评论。 Python 的字节码效率远低于 Java,因为它是一种动态语言,没有关于类型的编译时间信息,但脚本运行得更快。我也不明白为什么reduceLeft 需要这么长时间。我想也许 python 的 join 函数是 C 优化的,所以我用 python 的 reduce 替换它,它仍然只花了几分之一秒。也许我需要用 Java 重新编写应用程序,看看这是语言问题还是 JVM 问题。
  • 是的,这看起来很奇怪。您还没有回答@om-nom-nom 的评论:您是使用scalac 编译Scala 还是使用scala 作为脚本运行?
  • 另外,reduceLeft 在一个循环中连接字符串,会产生很多垃圾,而join 不需要;但当然同样适用于reduce...
【解决方案4】:

试试这个代码写入文件:

val f = new java.io.PrintWriter(new java.io.File("output1.txt"))
f.write(counters.reduce(_ + "\n" + _))
f.close()

快得多。

【讨论】:

    猜你喜欢
    • 2015-11-14
    • 1970-01-01
    • 1970-01-01
    • 2020-12-25
    • 2014-03-29
    • 2015-05-25
    • 2020-02-12
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多