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.
źródło
Odpowiedzi:
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:
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
for
w Scali jest implementowany poprzez wywołania metod i anonimowe klasy.W przypadku korzystania z klasy Java, takie jak
String
,Array
lubAnyVal
klas, które odpowiadają prymitywów Java, wolą metod przewidzianych przez Java, gdy istnieją alternatywy. Na przykład użyjlength
naString
iArray
zamiastsize
.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żAbstractFunction1
zamiast tego.Użyj
-optimise
i specjalizacji, aby uzyskać większość Scali.Zrozum, co się dzieje:
javap
jest 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ć
null
zamiastOption
ś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ę.
źródło
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.
źródło
Int
,Char
itp kiedy to możliwe. Podczas gdy pętle są tak samo wydajne w Scali.Function
klas. Jeśli przekażesz lambda domap
, 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) zapply
wywołań.scala.util.Random
to po prostu owijki wokół równoważnych klas JRE. Dodatkowe wywołanie funkcji jest nieco marnotrawstwem.java.lang.Math.signum(x)
jest znacznie bardziej bezpośredni niżx.signum()
, który konwertuje doRichInt
iz powrotem.źródło
skraca czas z 800 ms do 20 w moim systemie.
źródło