Scala против производительности Java (генерация HashSet и bigram)
Я столкнулся с некоторым несоответствием производительности между практически идентичными реализациями версий Scala и Java. Я вижу версию Java, которая на 68% быстрее, чем версия Scala. Любая идея относительно того, почему это происходит?
Версия 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.
В моем блоге есть дополнительная информация.
Ответы
Ответ 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 реализован как вызов метода apply() в Function0, который является вызовом метода мегагоризации (дорогостоящий с точки зрения JVM/JIT). Кроме того, возможно, некоторые оптимизации конкатенации строк, сделанные javac.
Я не проверял свои предположения, глядя на сгенерированный байт-код, но заменяя
и конкатенация строк с помощью StringBuilder сделала разницу незначительной.
Time for Java Version: 451 millis
Time for Scala Version: 589 millis
Ответ 2
Для -понимание всегда медленнее, чем использование цикла while
или хвостовой рекурсии как здесь.
Другая проблема в вашем примере - это конкатенация String
s. Scala будет использовать scala.collection.mutable.StringBuilder
, который имеет некоторые проблемы с производительностью (например, он будет помещать ваши экземпляры char
в char
), как указано в других ответах.
Изменив понимание для хвостового рекурсивного метода и используя 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 "1.8.0_20-ea"
Scalac
Scala версия компилятора 2.11.0 - Copyright 2002-2013, LAMP/EPFL
Scala
Scala версия для кода 2.11.0 - Copyright 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;
Но это не объясняет все это.
Ответ 4
Есть несколько способов дальнейшего ускорения кода Scala.
- Вместо использования StringBuilder вместо этого мы используем массив из 2 символов char
- Вместо создания временных vals x1 и x2 мы просто пишем непосредственно в массив char
- Затем мы используем конструктор String char [] для создания строки для размещения внутри HashSet
-
Мы извлекаем завершение цикла в переменную 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-коду,