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-коду,