我遇到了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还有更多信息。
答案 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代码的速度。
我们将循环终止提取到变量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代码,