【问题标题】:Scala vs Java performance (HashSet and bigram generation)Scala 与 Java 性能(HashSet 和二元组生成)
【发布时间】:2014-10-25 03:06:50
【问题描述】:

我在 Scala 和 Java 版本的实现之间遇到了一些几乎相同的性能差异。我看到 Java 版本比 Scala 版本快 68%。知道为什么会发生这种情况吗?

Java 版本:

public class Util {
public static Set < String > toBigramsJava(String s1) {
    Set <String> nx = new HashSet <String> ();
    for (int i = 0; i < s1.length() - 1; i++) {
        char x1 = s1.charAt(i);
        char x2 = s1.charAt(i + 1);
        String tmp = "" + x1 + x2;
        nx.add(tmp);
    }
    return nx;
}

}

Scala 版本:

object Util {
def toBigramsScala(str: String): scala.collection.mutable.Set[String] = {
    val hash: scala.collection.mutable.Set[String] = scala.collection.mutable.HashSet[String]()
    for (i <-0 to str.length - 2) {
        val x1 = str.charAt(i)
        val x2 = str.charAt(i + 1)
        val tmp = "" + x1 + x2
        hash.add(tmp)
    }
    return hash
}

}

测试结果:

scala> Util.time(for(i<-1 to 1000000) {Util.toBigramsScala("test test abc de")}) 17:00:05.034 [info] Something took: 1985ms

Util.time(for(i<-1 to 1000000) {Util.toBigramsJava("test test abc de")}) 17:01:51.597 [info] Something took: 623ms

系统:

我在 Ubuntu 14.04 上运行此程序,具有 4 个内核和 8Gig RAM。 Java 版本 1.7.0_45,Scala 版本 2.10.2。

我的blog 上有更多信息。

【问题讨论】:

  • 虽然这不是一个问题...您可以将其修改为一组匹配的问题和答案。
  • 我建议你看一下字节码,看看有什么区别。
  • 这可能是对 Scala 中不存在的 Java for 循环的优化,因为它们在 Scala 中有一些特殊性?这两种方法看起来确实相同。另外,如果将 scala.collection.mutable.HashSet 替换为 java.util.HashSet 会发生什么?
  • 你可能对我刚刚找到的这篇文章感兴趣。看来for循环确实是问题所在:ochafik.com/blog/?p=806
  • 对于它的价值,我只是对util.HashSetmutable.HashSetadd 方法做了一个快速的微基准测试。添加一个字符串或 100 个不同的字符串,我对两者的性能大致相同。所以在东部,我不认为这是 Scala 的可变 HashSet 的错。仅供参考,我的基准测试基于this example,它使用 Caliper 来避免 JVM 上微基准测试的常见缺陷。

标签: java performance scala


【解决方案1】:

我在这个 scala 版本中得到了大致相同的结果

object Util {
  def toBigramsScala(str: String) = {
    val hash = scala.collection.mutable.Set.empty[String]
    var i: Int = 0
    while (i <  str.length - 1) {
      val x1 = str.charAt(i)
      val x2 = str.charAt(i + 1)
      val tmp = new StringBuilder().append(x1).append(x2).toString()
      hash.add(tmp)
      i += 1
    }
    hash
  }
}

我记得 scala 中的 for 循环实现为对 Function0 上的 apply() 方法的调用,这是超态方法调用(从 JVM/JIT 的角度来看是昂贵的)。加上可能由 javac 进行的一些字符串连接优化。

我没有通过寻找生成的字节码来检查我的假设,而是用 while 替换 和 StringBuilder 的字符串连接使差异可以忽略不计。

Time for Java Version: 451 millis
Time for Scala Version: 589 millis

【讨论】:

    【解决方案2】:

    For-comprehensions 总是比使用 while 循环或尾递归作为 explained here 慢。

    您示例中的另一个问题是Strings 的串联。如其他答案中所述,Scala 将使用存在一些性能问题的 scala.collection.mutable.StringBuilder(例如,它将您的 chars 装箱到 Char 实例)。

    将 for-comprehension 更改为尾递归方法并使用java.lang.StringBuilder,您将在 Scala 和 Java 中获得几乎相同的结果(在我的机器上,Scala 实际上快了几毫秒)。

    【讨论】:

      【解决方案3】:

      我进行了类似的测试。

      以下是课程:

      Java

      public class JavaApp {
          public static void main(String[] args) {
              String s1 = args[0];
              java.util.Set <String> nx = new java.util.HashSet<>();
              for (int i = 0; i < s1.length() - 1; i++) {
                  char x1 = s1.charAt(i);
                  char x2 = s1.charAt(i + 1);
                  String tmp = "" + x1 + x2;
                  nx.add(tmp);
              }
              System.out.println(nx.toString());
          }
      }
      

      斯卡拉

      object ScalaApp {
          def main(args:Array[String]): Unit = {
              var s1 = args(0)
              val hash: scala.collection.mutable.Set[String] = scala.collection.mutable.HashSet[String]()
              for (i <-0 to s1.length - 2) {
                  val x1 = s1.charAt(i)
                  val x2 = s1.charAt(i + 1)
                  val tmp = "" + x1 + x2
                  hash.add(tmp)
              }
              println(hash.toString())
          }
      }
      

      编译器和运行时版本

      Javacjavac 1.8.0_20-ea

      Javajava版本“1.8.0_20-ea”

      Scalac Scala 编译器版本 2.11.0 -- 版权所有 2002-2013,LAMP/EPFL

      Scala Scala 代码运行器版本 2.11.0 -- 版权所有 2002-2013,LAMP/EPFL

      Scala 也比较慢。看看Scala 版本,它创建了两个匿名类。

      可能需要一些时间的一件事是auto boxingfor 循环中的char 变量上。

        44: iload_2
        45: invokestatic  #61                 // Method scala/runtime/BoxesRunTime.boxToCharacter:(C)Ljava/lang/Character;
        48: invokevirtual #55                 // Method scala/collection/mutable/StringBuilder.append:(Ljava/lang/Object;)Lscala/collection/mutable/StringBuilder;
        51: iload_3
        52: invokestatic  #61                 // Method scala/runtime/BoxesRunTime.boxToCharacter:(C)Ljava/lang/Character;
        55: invokevirtual #55                 // Method scala/collection/mutable/StringBuilder.append:(Ljava/lang/Object;)Lscala/collection/mutable/StringBuilder;
      

      但这并不能解释一切。

      【讨论】:

        【解决方案4】:

        有一些方法可以进一步加快 Scala 代码的速度。

        1. 我们不使用 StringBuilder,而是使用 2 个字符的 char 数组
        2. 我们没有创建临时 val x1 和 x2,而是直接写入 char 数组
        3. 然后我们使用 String 的 char[] 构造函数来创建要放置在 HashSet 中的字符串
        4. 我们将循环终止提取到一个变量 max 中,以防 JIT 错过优化它。

             object Util {
               def toBigramsScala(str: String) = {
                 val hash = scala.collection.mutable.HashSet.empty[String]
                 val charArray = new Array[Char](2)
                 var i = 0
                 val max = str.length - 1
                 while (i < max) {
                   charArray(0) = str.charAt(i)
                   charArray(1) = str.charAt(i + 1)
                   hash.add(new String(charArray))
                   i += 1
                 }
                 hash
               }
             }
          

        通过这些更改,我能够在 Java 和 Scala 代码之间获得相同的运行时间。令人惊讶的是(至少在这个例子中),java.util.HashSet 并没有比 mutable.HashSet 带来任何性能提升。公平地说,我们也可以将所有这些优化应用到 Java 代码中,

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-04-29
          • 2011-09-17
          • 2017-04-14
          • 1970-01-01
          • 2013-11-30
          相关资源
          最近更新 更多