Wydajność Scali w porównaniu z Javą

41

Przede wszystkim chciałbym wyjaśnić, że nie jest to pytanie język-X-język-Y, aby ustalić, który jest lepszy.

Używam Javy od dłuższego czasu i zamierzam nadal z niej korzystać. Równolegle uczę się Scali z wielkim zainteresowaniem: poza drobnymi rzeczami, które przyzwyczajają się do mojego wrażenia, to, że naprawdę mogę bardzo dobrze pracować w tym języku.

Moje pytanie brzmi: w jaki sposób oprogramowanie napisane w Scali różni się od oprogramowania napisanego w Javie pod względem szybkości wykonywania i zużycia pamięci? Oczywiście trudno jest odpowiedzieć na to pytanie, ale spodziewałbym się, że konstrukcje wyższego poziomu, takie jak dopasowanie wzorca, funkcje wyższego rzędu itp., Wprowadzą pewne narzuty.

Jednak moje obecne doświadczenie w Scali jest ograniczone do małych przykładów poniżej 50 linii kodu i do tej pory nie przeprowadziłem żadnych testów porównawczych. Więc nie mam prawdziwych danych.

Jeśli okazało się, że Scala ma trochę narzutów w Javie, czy ma sens mieszane projekty Scala / Java, w których koduje się bardziej złożone części w Scali i części krytyczne pod względem wydajności w Javie? Czy to powszechna praktyka?

EDYCJA 1

Przeprowadziłem mały test porównawczy: zbuduj listę liczb całkowitych, pomnóż każdą liczbę całkowitą przez dwa i umieść ją na nowej liście, wydrukuj wynikową listę. Napisałem implementację Java (Java 6) i implementację Scala (Scala 2.9). Uruchomiłem oba na Eclipse Indigo pod Ubuntu 10.04.

Wyniki są porównywalne: 480 ms dla Javy i 493 ms dla Scali (średnio ponad 100 iteracji). Oto fragmenty, których użyłem.

// Java
public static void main(String[] args)
{
    long total = 0;
    final int maxCount = 100;
    for (int count = 0; count < maxCount; count++)
    {
        final long t1 = System.currentTimeMillis();

        final int max = 20000;
        final List<Integer> list = new ArrayList<Integer>();
        for (int index = 1; index <= max; index++)
        {
            list.add(index);
        }

        final List<Integer> doub = new ArrayList<Integer>();
        for (Integer value : list)
        {
            doub.add(value * 2);
        }

        for (Integer value : doub)
        {
            System.out.println(value);
        }

        final long t2 = System.currentTimeMillis();

        System.out.println("Elapsed milliseconds: " + (t2 - t1));
        total += t2 - t1;
    }

    System.out.println("Average milliseconds: " + (total / maxCount));
}

// Scala
def main(args: Array[String])
{
    var total: Long = 0
    val maxCount    = 100
    for (i <- 1 to maxCount)
    {
        val t1   = System.currentTimeMillis()
        val list = (1 to 20000) toList
        val doub = list map { n: Int => 2 * n }

        doub foreach ( println )

        val t2 = System.currentTimeMillis()

        println("Elapsed milliseconds: " + (t2 - t1))
        total = total + (t2 - t1)
    }

    println("Average milliseconds: " + (total / maxCount))
}

Tak więc w tym przypadku wydaje się, że narzut Scala (użycie zasięgu, mapy, lambda) jest naprawdę minimalny, co nie jest dalekie od informacji dostarczonych przez Inżyniera Świata.

Może istnieją inne konstrukcje Scali, które należy stosować ostrożnie, ponieważ są szczególnie ciężkie do wykonania?

EDYCJA 2

Niektórzy z was zwrócili uwagę, że println w wewnętrznych pętlach zajmuje większość czasu wykonania. Usunąłem je i ustawiłem rozmiar list na 100000 zamiast 20000. Uzyskana średnia wyniosła 88 ms dla Javy i 49 ms dla Scali.

Giorgio
źródło
5
Wyobrażam sobie, że skoro Scala kompiluje się do kodu bajtowego JVM, to teoretycznie wydajność może być równoważna Javie działającej pod tym samym JVM, przy czym wszystkie inne rzeczy są równe. Myślę, że różnica polega na tym, jak kompilator Scala tworzy kod bajtowy i czy robi to skutecznie.
maple_shaft
2
@maple_shaft: A może czas kompilacji Scali wiąże się z dodatkowymi kosztami?
FrustratedWithFormsDesigner
1
@Giorgio Nie ma rozróżnienia między obiektami Scala a obiektami Java, wszystkie są obiektami JVM, które są zdefiniowane i zachowują się zgodnie z kodem bajtu. Scala na przykład jako język ma pojęcie zamknięć, ale kiedy są one kompilowane, są kompilowane do wielu klas z kodem bajtowym. Teoretycznie mógłbym fizycznie napisać kod Java, który mógłby się skompilować do dokładnie tego samego kodu bajtowego, a zachowanie środowiska wykonawczego byłoby dokładnie takie samo.
wałek klonowy
2
@maple_shaft: Właśnie do tego dążę: powyższy kod Scala jest o wiele bardziej zwięzły i czytelny niż odpowiedni kod Java. Zastanawiałem się tylko, czy sensowne byłoby pisanie części projektu Scala w Javie ze względu na wydajność i jakie będą te części.
Giorgio
2
Środowisko wykonawcze będzie w dużej mierze zajmowane przez wywołania println. Potrzebujesz bardziej intensywnego testu.
kevin cline

Odpowiedzi:

39

Jest jedna rzecz, którą możesz zrobić zwięźle i skutecznie w Javie, czego nie możesz zrobić w Scali: wyliczenia. W przypadku wszystkich innych elementów, nawet w przypadku wolno działających konstrukcji w bibliotece Scali, można uzyskać wydajne wersje działające w Scali.

W większości przypadków nie musisz dodawać Java do swojego kodu. Nawet w przypadku kodu korzystającego z wyliczenia w Javie często istnieje Scala odpowiednie lub dobre rozwiązanie - umieszczam wyjątek na wyliczeniach, które mają dodatkowe metody i których wartości int są stałe.

Jeśli chodzi o to, na co należy uważać, oto kilka rzeczy.

  • Jeśli używasz wzorca wzbogacania mojej biblioteki, zawsze konwertuj na klasę. Na przykład:

    // WRONG -- the implementation uses reflection when calling "isWord"
    implicit def toIsWord(s: String) = new { def isWord = s matches "[A-Za-z]+" }
    
    // RIGHT
    class IsWord(s: String) { def isWord = s matches "[A-Za-z]+" }
    implicit def toIsWord(s: String): IsWord = new IsWord(s)
    
  • Uważaj na metody zbierania - ponieważ są one w większości polimorficzne, JVM ich nie optymalizuje. Nie musisz ich unikać, ale zwróć na to uwagę w krytycznych sekcjach. Należy pamiętać, że forw Scali jest implementowany poprzez wywołania metod i anonimowe klasy.

  • W przypadku korzystania z klasy Java, takie jak String, Arraylub AnyValklas, które odpowiadają prymitywów Java, wolą metod przewidzianych przez Java, gdy istnieją alternatywy. Na przykład użyj lengthna Stringi Arrayzamiast size.

  • Unikaj nieostrożnego korzystania z niejawnych konwersji, ponieważ możesz znaleźć konwersję przypadkowo zamiast projektowo.

  • Rozszerz klasy zamiast cech. Na przykład, jeśli przedłużasz Function1, przedłuż AbstractFunction1zamiast tego.

  • Użyj -optimisei specjalizacji, aby uzyskać większość Scali.

  • Zrozum, co się dzieje: javapjest twoim przyjacielem, podobnie jak kilka flag Scala, które pokazują, co się dzieje.

  • Idiomy Scala zostały zaprojektowane w celu poprawy poprawności i uczynienia kodu bardziej zwięzłym i łatwym w utrzymaniu. Nie są przeznaczone do prędkości, więc jeśli chcesz użyć nullzamiast Optionścieżki krytycznej, zrób to! Jest powód, dla którego Scala jest paradygmatem.

  • Pamiętaj, że prawdziwą miarą wydajności jest uruchamianie kodu. Zobacz to pytanie, aby zobaczyć przykład, co może się stać, jeśli zignorujesz tę zasadę.

Daniel C. Sobral
źródło
1
+1: Wiele przydatnych informacji, nawet na tematy, które wciąż muszę się uczyć, ale dobrze jest przeczytać jakąś wskazówkę, zanim będę mógł na nie spojrzeć.
Giorgio
Dlaczego pierwsze podejście wykorzystuje refleksję? I tak generuje anonimową klasę, więc dlaczego nie użyć jej zamiast refleksji?
Oleksandr.Bezhan
@ Oleksandr.Bezhan Anonimowa klasa to koncepcja Java, a nie Scala. Generuje dopracowanie typu. Do anonimowej metody klasowej, która nie zastępuje swojej klasy podstawowej, nie można uzyskać dostępu z zewnątrz. To samo nie dotyczy udoskonaleń typu Scali, więc jedynym sposobem na uzyskanie tej metody jest odbicie.
Daniel C. Sobral
Brzmi okropnie. W szczególności: „Uważaj na metody zbierania - ponieważ są one w większości polimorficzne, JVM ich nie optymalizuje. Nie musisz ich unikać, ale zwróć na to uwagę w krytycznych sekcjach”.
mat
21

Według Benchmarks Game dla jednego rdzenia, 32-bitowego systemu, Scala ma medianę 80% szybciej niż Java. Wydajność jest w przybliżeniu taka sama dla komputera Quad Core x64. Nawet użycie pamięci i gęstość kodu są bardzo podobne w większości przypadków. Powiedziałbym na podstawie tych (raczej nienaukowych) analiz, że masz rację twierdząc, że Scala dodaje pewne koszty ogólne do Javy. Wydaje się, że nie dodaje ton napowietrznych, więc podejrzewam, że diagnoza przedmiotów wyższego rzędu zajmujących więcej miejsca / czasu jest najbardziej poprawna.

Inżynier świata
źródło
2
Aby uzyskać tę odpowiedź, użyj bezpośredniego porównania, jak sugeruje strona pomocy ( shootout.alioth.debian.org/help.php#comparetwo )
igouy
18
  • Wydajność Scali jest bardzo przyzwoita, jeśli po prostu piszesz kod podobny do Java / C w Scali. Kompilator użyje JVM dla prymitywów Int, Charitp kiedy to możliwe. Podczas gdy pętle są tak samo wydajne w Scali.
  • Należy pamiętać, że wyrażenia lambda są kompilowane do instancji anonimowych podklas Functionklas. Jeśli przekażesz lambda do map, anonimowa klasa musi zostać utworzona (a niektóre lokalizacje mogą wymagać przekazania), a następnie każda iteracja ma dodatkowe narzuty wywołania funkcji (z pewnym przekazywaniem parametrów) z applywywołań.
  • Wiele takich klas scala.util.Randomto po prostu owijki wokół równoważnych klas JRE. Dodatkowe wywołanie funkcji jest nieco marnotrawstwem.
  • Uważaj na implikacje w kodzie krytycznym dla wydajności. java.lang.Math.signum(x)jest znacznie bardziej bezpośredni niż x.signum(), który konwertuje do RichIntiz powrotem.
  • Główną przewagą wydajności Scali nad Javą jest specjalizacja. Należy pamiętać, że specjalizacja jest oszczędnie wykorzystywana w kodzie biblioteki.
Daniel Lubarov
źródło
5
  • a) Z mojej ograniczonej wiedzy muszę zauważyć, że kodu w statycznej metodzie głównej nie można optymalizować bardzo dobrze. Powinieneś przenieść kod krytyczny w inne miejsce.
  • b) Na podstawie długich obserwacji odradzam wykonywanie dużej wydajności podczas testu wydajności (z wyjątkiem tego, co chcesz zoptymalizować, ale kto powinien przeczytać 2 miliony wartości?). Mierzysz println, co nie jest zbyt interesujące. Zamiana println na max:
(1 to 20000).toList.map (_ * 2).max

skraca czas z 800 ms do 20 w moim systemie.

  • c) Wiadomo, że zrozumienie jest trochę powolne (musimy przyznać, że cały czas się poprawia). Zamiast tego użyj funkcji while lub tailrecursive. Nie w tym przykładzie, w którym jest to zewnętrzna pętla. Użyj @ tailrec-adnotation, aby przetestować tęirecursivity.
  • d) Porównanie z C / Asemblerem kończy się niepowodzeniem. Na przykład nie przepisujesz kodu Scala dla różnych architektur. Inną ważną różnicą w stosunku do sytuacji historycznych są
    • Kompilator JIT, optymalizacja w locie, a może dynamicznie, w zależności od danych wejściowych
    • Znaczenie braków w pamięci podręcznej
    • Rosnące znaczenie równoległego wywoływania. Obecnie Scala oferuje rozwiązania do pracy bez większych kosztów równoległych. W Javie nie jest to możliwe, chyba że wykonujesz znacznie więcej pracy.
nieznany użytkownik
źródło
2
Usunąłem println z pętli, a kod Scali jest szybszy niż kod Java.
Giorgio
Porównanie z C i asemblerem miało następujące znaczenie: język wyższego poziomu ma silniejsze abstrakcje, ale może być konieczne użycie języka niższego poziomu do wykonania. Czy ta paralela utrzymuje się, że Scala jest językiem wyższego poziomu, a Java niższym językiem? Może nie, ponieważ Scala wydaje się zapewniać wydajność podobną do Javy.
Giorgio
Nie sądziłbym, że miałoby to duże znaczenie dla Clojure lub Scali, ale kiedy bawiłem się z jRuby i Jython, prawdopodobnie napisałbym bardziej krytyczny kod wydajności w Javie. Z tymi dwoma zauważyłem znaczną różnicę, ale to było lata temu ... mogłoby być lepiej.
Rig