Scala与Java性能(HashSet和bigram生成)

时间:2014-08-31 21:31:03

标签: java performance scala

我遇到了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还有更多信息。

4 个答案:

答案 0 :(得分:10)

这个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中的循环实现为在Function0上调用apply()方法,这是一个变形方法调用(从JVM / JIT的角度来看代价很高)。加上javac可能会进行一些字符串连接优化。

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

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

答案 1 :(得分:4)

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

您的示例中的另一个问题是String的连接。 Scala会使用scala.collection.mutable.StringBuilder,这会遇到一些性能问题(例如,它会将您的char打包到Char个实例),如其他答案中所述。

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

答案 2 :(得分:3)

我进行了类似的测试。

以下是课程:

爪哇

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());
    }
}

Scala的

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())
    }
}

编译器和运行时版本

Javac javac 1.8.0_20-ea

Java java版“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 boxing循环中char变量的for

  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;

但这并不能解释这一切。

答案 3 :(得分:0)

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

  1. 我们不使用StringBuilder,而是使用2个字符的char数组
  2. 我们只是直接写入char数组
  3. ,而不是创建临时值x1和x2
  4. 然后我们使用String&#39的char []构造函数创建放置在HashSet中的字符串
  5. 我们将循环终止提取到变量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
         }
       }
    
  6. 通过这些更改,我能够在Java和Scala代码之间获得相同的运行时间。令人惊讶的是(至少在这个例子中),java.util.HashSet没有比mutable.HashSet提供任何性能提升。公平地说,我们也可以将所有这些优化应用于Java代码,