poniedziałek, stycznia 11, 2010

Plus dla Scala, minus dla Groovy ;-)

W ramach przyglądania się różnym językom zacząłem oglądać Scale i Groovy, żeby sprawdzić jak się w tym pisze małe rzeczy napisałem sobie moje "ulubione" całkowanie numeryczne [to takie moje Hello World].

W Scala wygląda tak:
  object Speed {
var N=10000
var M=1000

def calc():Double = {
var sum:Double = 0.0;
for (i <- 0 until N) {
var x:Double=20.0*i/N-10.0;
sum+=Math.pow(Math.E,-x*x)*(20.0/N);
}
return sum;
}

def main(args: Array[String]) = {
var s:Double=0;
var start:Long=System.currentTimeMillis();
for (i <- 0 until M) {
s+=calc();
}
println((System.currentTimeMillis()-start)/(M*1.0));
}
}

W Groovy tak:
N=10000;
M=1000;

def double calc() {
double sum=0;
for (i in 0..N) {
double x = 20.0*i/N-10.0;
sum+=Math.pow(Math.E, -x*x)*20.0/N;
}
return sum;
}

long start = System.currentTimeMillis();
double s = 0;
for (i in 0..M) {
s+=calc();
}
println((System.currentTimeMillis()-start)/(M*1.0));

Trudno powiedzieć która składnia jest bardziej czytelna, na pewno bardziej mi się podoba zapis pętli for w Groovy.

Jednak okazało się, że Groovy poległo na całej linii jeśli chodzi o wydajność ;-)
Scala [na JVM] potrzebowała na jedno wykonanie funkcji calc() średnio 5.15 ms, a Groovy na to samo potrzebowało aż 138.79 ms! Dla porównania Java potrzebowała 4.84 ms.

Na razie nie czuję się przekonany do żadnego z tych języków ;-) wydaje się tak na pierwszy rzut oka, że Scala jest taka jakby dojrzalsza, a Groovy choć bardziej podobne do Java'y to tak celuje w język skryptowy.


Podobne postybeta
Raspberry Pi to nie jest demon prędkości ;-)
Całkujący Dart ;-)
C# i Java okazały się szybsza od Pythona :-) [było Java okazała się szybsza od C# i Pythona]
ETA - liczymy szacowany czas zakończenia [przybycia ;-)]
Go wolniejsze od C i JavaScript, i ciut szybsze niż Java ;-) [a jednak od Java'y też wolniejsze]

13 komentarzy:

  1. Bardziej idiomatyczna wersja Scali (będzie ciut wolniejsza przez boxing Double'i):
    http://gist.github.com/274519

    OdpowiedzUsuń
  2. Er, zapomniałem zmienić vary na vale

    OdpowiedzUsuń
  3. To Twoje rozwiązanie ma jakiś urok :-) ale jak dla mnie taka konstrukcja:
    def calc = (0.0 /: (0 until N)) {(s, i) =>
    val x = 20.0 * i / N - 10.0
    s + Math.pow(Math.E, -x * x) * (20.0 / N)
    }
    wygląda strasznie nieczytelnie.
    rozumiem, ze s ma być zainicjalizowane wartością 0.0, po czym i ma przybierać wartości od 0 do N [bez N jak mniemam], następnie do x ma być przypisany wyni, a s ma zostać zwiększone.......
    Do końca taki paradygmat do mnie nie przemawia :-) choć to pewnie sprawa przyzwyczajenia.
    A co do prędkości to ta wersja jest tylko nieznacznie [o dziesiątki mikrosekund wolniejsza od wersji którą ja przedstawiłem]

    OdpowiedzUsuń
  4. Zamiast:
    (0.0 /: (0 until N)) {(s, i) => ...

    możesz:
    (1 until 10).foldLeft(0.0){(akumulator, i) => ...}

    /: jest prawie aliasem foldLeft. Metody kończące się dwukropkiem aplikuje się odwrotnie (receiver po prawej stronie) Oczywiście notacją z kropką można wymusić:

    1 :: 2 :: Nil

    to to samo co:

    Nil.::(2).::(1)

    Co do przyzwyczajeń to oczywiście zgadzam się. Jednak jak zaczniesz pisać więcej używając paradygmatu funkcyjnego ten zapis stanie się dla Ciebie bardziej naturalny niż imperatywna pętla.

    Dla przykładu, mając listę zwiększ wszystkie elementy o jeden, wybierz elementy większe od 3 i oblicz ich sumę:

    List(5, 1, 10) map (1+) filter (3<) reduceLeft(_+_)

    OdpowiedzUsuń
  5. Groovy jest interpretowany, Scala kompilowana. To jakbyś porównywał Pythona z C++.

    A ja bym to w Scali napisał tak: http://gist.github.com/276105

    PS przemelek, zrobiłbyś jakiś markdown w komentarzach, można by tu wklejać kawałki kodu zamiast linkować w świat.

    OdpowiedzUsuń
  6. @Mekk wpadłem na to, że Groovy jest interpretowane, a Scala kompilowana, po prostu myślałem, że oba są kompilowane. No i okazuje się, że to porównanie Pythona z C++ nie do końca jest słuszne bo po pierwsze interpretowany Python był jednak szybszy niż Groovy ;-) po drugie gdy go skompilować JITem to też znacznie przyśpiesza :-)

    Z tego co mi wiadomo to "se ne da", i tak jest lepiej niż standardowo w BlogSpot'cie bo mam ustawione komentarze pod postem, a standardowo wyskakuje w ogóle oddzielne okienko.

    OdpowiedzUsuń
  7. To że groovy jest językiem dynamicznym to nie znaczy że nie jest kompilowany. Groovy traci na rzecz scali oczywiście przez dynamic dispatch. Btw, pokaż kod dla którego groovy jest wolniejszy od pythona.

    OdpowiedzUsuń
  8. Kod w Groovy masz powyżej :-), jedna iteracja calc() wykonuje się średnio jak napisałem wyżej 138.79 ms, kod w Pythonie [samą funkcję calc()] znajdziesz w tym poście http://przemelek.blogspot.com/2009/10/java-okazaa-sie-szybsza-od-c-i-pythona.html [albo 1 link w sekcji podobne posty powyżej] i jedna iteracja calc() wykonuje się 11.7-11.8 ms, czyli 13 razy szybciej w Pythonie niż w Groovy :-)

    Tutaj jak Ci się nie chce zaglądać do tego drugiego posta masz kod w Pythonie, mam nadzieje, że formatowania się zbytnio nie popsują ;-)

    import time
    import math

    N=10000
    M=1000

    def calc():
    sum=0
    for i in range(0,N):
    x=20.0*i/N-10
    sum=sum+math.e**(-x**2)*(20.0/N)
    return sum

    start = time.time()
    for i in range(0,M):
    calc()
    print ((time.time()-start)*1000/M)

    OdpowiedzUsuń
  9. No to wszystko jasne. Mimo że groovy ssie jak nie wiem to i tak okazał sie nieznacznie szybszy od pythona.
    Dwie rzeczy, a nawet trzy:

    double x = 20.0*i/N-10.0;

    Tutaj w pętli tworzony jest tymczasowy BigDecimal (dla operacji "/"), który jest niesamowicie ciężki.

    Kolejna sprawa, po zsamplowaniu tego przykładu:
    Stub + native Method
    58.4% 0 + 770 java.lang.Throwable.fillInStackTrace
    5.5% 64 + 9 calc.calc
    2.4% 28 + 3 java.math.BigDecimal.setScale

    Compiled + native Method
    6.2% 82 + 0 ExceptionBlob

    Interpreted + native Method
    3.6% 1 + 46
    groovy.lang.MetaClassImpl.checkIfStdMethod
    1.0% 0 + 13 java.lang.Throwable.fillInStackTrace

    Czyli właściwie 5.5% dzieje się w calc, a 70% czasu java traci na tworzeniu stacktrace'a i rzucaniu jakiegoś wyjątku w bebechach (prawdopodobnie w BigDecimal)

    Wreszcie, czy testy prowadzisz na kompilatorze serwerowym hotspota ? i czy grzejesz te przykłady ?
    W przeciwnym przypadku wszystkie testy uruchamiane do tej pory na jvm można wyrzucić do śmieci...

    N=10000
    M=1000

    def double calc() {
    double sum=0 // dzięki temu zapobiegasz tworzeniu BigDecimala
    double DN=N // ...
    for (i in 0..N) {
    double di = i
    double x = 20.0*di/DN-10.0;
    sum+=Math.pow(Math.E, -x*x)*20.0/DN;
    }
    return sum;
    }

    def bench() {
    long start = System.currentTimeMillis();
    double s = 0
    for (i in 0..M) {
    s+=calc();
    }
    println((System.currentTimeMillis()-start)/(M*1.0));
    }

    bench()
    bench()
    bench()
    bench()

    Powyższy przykład można oczywiście jeszcze przyśpieszyć...

    OdpowiedzUsuń
  10. Jeśli odpalić Twój program w Groovy z przełącznikiem serwer to jest trochę szybszy od Pythona :-) bo średnia z 4 wypisanych wyników to 10.09 ms, dla Pythona jest to 10.74 ms.
    Tak się przy okazji pobawiłem przełącznikiem -server i mnie Java zaskoczyła :-) Java 1.5 z przełącznikiem -server potrzebowała średnio 1.34 ms, ale już 1.6 3.67 ms... choć fakt, że chodzi mi po głowie coś o jakimś zamieszaniu z arytmetyką zmiennoprzecinkową.

    OdpowiedzUsuń
  11. 1.5 jest szybsza od 1.6 po rozgrzaniu ? To też byłaby niespodzianka...

    OdpowiedzUsuń
  12. Tak sobie czytam tego posta oraz komentarze i zastanawiam się jak wielu z was na codzień rozwiązuje numeryczne zadania w pracy a ilu z was używa języków programowania do pobrania inputu od usera, sprawdzenia go i wygenerowania outputu na bazie odczytów z bazy danych...
    Te 130ms są niczym wobec jednego zapytania do bazy danych, która jest na drugim końcu świata (albo labu :D)...
    Co do wydajności to zgadzam się, że Groovy jest wolniejszy od Scali/Javy - ale nie jest to język do tego samego... Groovy lepiej nadaje się do deklarowania rzeczy (konfiguracje, opis funkcjonalności, DSLe - patrz Grails) a sama Java/Scala lepiej nadaje się do zastowań numerycznych (czego i tak jest znacznie mniej w ogólnym rozrachunku...)

    Moim zdaniem np. Lift czy tym podobne rozwiązania ze statycznym językiem pod spodem nigdy nie będą tak eleganckie jak platforma oparta o język dynamiczny.

    OdpowiedzUsuń