2014-08-31 13 views
12

Wpadłem na rozbieżności w wydajności pomiędzy niemal identycznymi implementacjami wersji Scala i Java. Widzę wersję Java, która jest 68% szybsza niż wersja Scala. Masz pomysł, dlaczego tak się dzieje? WersjaWydajność Scala vs Java (HashSet i generacja bigram)

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

} wersja

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 
} 

}

Wyniki testów:

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

System:

wpadłem to na Ubuntu 14.04, z 4 rdzeni i 8Gig RAM. Wersja Java 1.7.0_45, Scala wersja 2.10.2.

Istnieje więcej informacji na temat mojej blog.

+2

Nie jest to jednak pytanie ... Można je skorygować w pasującym zestawie pytań i odpowiedzi. –

+1

Proponuję zapoznać się z kodem bajtu, aby zobaczyć różnicę. –

+1

Czy może to być optymalizacja pętli Java 'for', która nie istnieje w Scali, ponieważ mają pewne cechy szczególne w Scali? Te dwie metody wydają się naprawdę identyczne. Co się stanie, jeśli zastąpisz scala.collection.mutable.HashSet przy pomocy java.util.HashSet? – Dici

Odpowiedz

10

Mam mniej więcej takie same wyniki z tego scala wersji

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

jak pamiętam do pętli w Scala wdrożone jako wezwanie do stosowania() metoda na Function0 która megamorphic wywołanie metody (droższe od JVM/Punkt widzenia JIT). Plus prawdopodobnie pewna optymalizacja łączenia łańcuchów przez javac.

Nie sprawdziłem swoich założeń, szukając wygenerowanego kodu bajtu, ale zastępując go podczas gdy , a konkatenacja ciągów za pomocą StringBuilder sprawiła, że ​​różnica była znikoma.

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

Przeprowadziłem podobny test.

Oto klas:

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

kompilatorów i wersja środowiska wykonawczego

Javac javac 1.8.0_20-ea

Java wersja java "1.8.0_20-ea"

Scalac Scala kompilator wersja 2.11.0 - Prawa autorskie 2002-2013, LAMP/EPFL

Scala Scala Kod biegacz wersja 2.11.0 - Prawa autorskie 2002-2013 , LAMP/EPFL

Scala jest również wolniejsza. Patrząc na wersję Scala, tworzy dwie anonimowe klasy.

Jedną rzeczą, która może być trochę zajęta, jest również auto boxing dla zmiennej w pętli 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; 

Ale to nie wyjaśnia wszystkiego.

4

For-Ułatwienia zawsze mniejsza a następnie za pomocą pętli, albo ogon while rekursji jako explained here.

Innym problemem w twoim przykładzie jest konkatenacja s String. Scala użyje nazwy scala.collection.mutable.StringBuilder, która ma pewne problemy z wydajnością (np. Będzie wyświetlać instancje char s do Char), jak wspomniano w innych odpowiedziach.

Zmiana sposobu rozumienia na metodę rekurencyjno-ogonową i użycie java.lang.StringBuilder spowoduje, że uzyskasz w większości wyniki zarówno Scala, jak i Java (na moim komputerze Scala jest w rzeczywistości o kilka milisekund szybsza).

0

Istnieje kilka sposobów na przyspieszenie kodu Scala.

  1. Zamiast używać StringBuilder, że zamiast korzystać z tablicy char 2 znaków
  2. Zamiast tworzyć tymczasowe Vals X1 i X2, po prostu pisać bezpośrednio do tablicy char
  3. Następnie używamy char STRING [] konstruktor tworzący ciąg znaków do umieszczenia wewnątrz HashSet
  4. Wyodrębniamy zakończenie pętli do zmiennej max, na wypadek, gdyby JIT pominęło jej optymalizację.

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

Z tych zmian, udało mi się uzyskać ten sam czas pracy między kodu Java i Scala. Zaskakująco (przynajmniej w tym przykładzie) java.util.HashSet nie dał żadnego wzrostu wydajności w stosunku do mutable.HashSet. W uczciwości możemy zastosować wszystkie te optymalizacje również do kodu Java,