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

12

我在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内存。Java版本为1.7.0_45,Scala版本为2.10.2。

有关更多信息,请参见我的博客


2
这不是一个问题... 你可以将其修改为一组匹配的问题和答案。 - Anubian Noob
1
我建议您查看字节码以查看区别。 - Peter Lawrey
1
这可能是Java中的优化for循环在Scala中不存在的原因,因为它们在Scala中具有某些特殊性?这两种方法看起来非常相似。另外,如果您将scala.collection.mutable.HashSet替换为java.util.HashSet会发生什么? - Dici
1
你可能会对我刚找到的这篇文章感兴趣。似乎for循环确实是问题所在:http://ochafik.com/blog/?p=806 - Dici
2
就我所知,我刚刚对util.HashSetmutable.HashSetadd方法进行了快速微基准测试。添加一个字符串或100个不同的字符串,两者的性能大致相同。因此,我认为这不是Scala可变HashSet的问题。顺便说一下,我基于此示例进行了基准测试,使用Caliper避免了JVM上微基准测试的常见陷阱。 - Cyäegha
显示剩余3条评论
4个回答

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中的for循环是在Function0上调用apply()方法实现的,这是一种多态方法调用(从JVM/JIT的角度来看很昂贵)。此外,Java编译器可能会进行一些字符串连接优化。

我没有通过查看生成的字节码来检查我的假设,但是用while替换for以及使用StringBuilder替换字符串连接可以使差异变得微不足道。

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

4

使用 while 循环或尾递归总是比使用 for-comprehensions 慢,如此处所述。

您示例中的另一个问题是字符串的连接。Scala 将使用 scala.collection.mutable.StringBuilder,它存在一些性能问题(例如,它会将您的 char 装箱为 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());
    }
}

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 version "1.8.0_20-ea"

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

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

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

还��一件可能需要一些时间的事情是在for循环中对char变量进行auto boxing转换。

  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;

但这并不是全部的解释。


1

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

  1. Instead of using a StringBuilder, we instead use a 2 character char array
  2. Instead of creating temporary vals x1 and x2, we just write directly to the char array
  3. We then use String's char[] constructor to create the string to place inside the HashSet
  4. We extract the loop termination into a variable max, just in case the JIT would miss optimizing that.

       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代码。


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