środa, 22 czerwca 2016

Prymitywna szybkość i lekkość

W Java8 są takie wynalazki jak interfejs ObjIntConsumer a nie ma na przykład funkcji/metody zip. Metoda/funkcja zip (takie czasy, ze już nie wiadomo czy funkcja czy metoda) nie zwraca kodu pocztowego ale w teorii łączyłaby dwa strumienie/kolekcje i jest ona o tyle ciekawa, że np. w takich tutorialach do Haskella pojawia się co krok a szczególnie wygląda fantastycznie w niezwykle praktycznym przykładzie implementacji ciągu Fibonacciego na strumieniach (naprawdę jest fajne - nie ma stackoverflow i w ogóle.)

Brak zipa to (podobno) jedna z konsekwencji tego, że mamy Stream,IntStream,DoubleStream,LongStream - po co to i co to daje? Ano coś daje -> patrzymy dalej ->

Patrz stary jakie pomiary

Mam dwa takie chytrze spreparowane kawałki kodu aby pokazać co dają "prymitywne strumienie" w Java8. Pierwszy kawałek kodu jest przeznaczony dla "wszystko jest obiektem" lajfstajlu :

long numberOfElements = 1000000000;

Long result = Stream
             .iterate(0L, i -> i + 1)
              .map(i->i*i)
              .limit(numberOfElements)
              .reduce(0L,Long::sum);

Drugi zaś kawałek ma za zadanie pokazać jak na pierdyliard sposobów w javie można zdefiniować funkcje która dodaje jeden (w zależności od typu, fazy księżyca i aktualnej pozycji reprezentacji Polski w grupie)

long numberOfElements = 1000000000;

LongUnaryOperator step = i -> i + 1;
LongUnaryOperator intSquare = i -> i * i;
LongBinaryOperator sum = (i1, i2) -> i1 + i2;

long primitiveResult = LongStream
            .iterate(0, step)
            .map(intSquare)
            .limit(numberOfElements)
            .reduce(0, sum);

Dalsza część zabawy wygląda podobnie jak laborki w technikum - "wiemy, że prąd działa jak prąd bo mamyw domu pralkę i telewizję ale zrobimy pomiar by to potwierdzić. A jak się nie będzie zgadzało to będziemy robić do skutku".

Ogólnie pomiary wydajności to dosyć delikatna sprawa bo niby każdy powinien to umieć robić ale zazwyczaj przy większości prezentacji o "Performensie" czy jakimś tam tutorialu zazwyczaj jest teza, że większośc ludzi robi to źle. Sam robiłem to źle kiedyś bo kiedy skończyłem studia ze średnią 4coś i zerowa wiedzą jak działa JVM to brałem po prostu System.miliseconds a później klasyk "end minus start".

Później po odkryciu, że jest coś takiego jak JIT, kod Javy w sumie nie do końca jest interpretowany a suchary o tym, że java jest wolna są powielane przez programistów c++ , którzy swoja tożsamość budują nad ilością kontroli, którą sprawują nad kawałkiem pamięci - po tym wszystkim okazało się, że w sumie trudno stwierdzić czy pomiary wydajność kodu javy zrobiło się dobrze bo nie wiadomo jak ma się to co jest napisane do tego co chodzi tam w rantaimie.

(Dodatkowo dobiło mnie kiedyś jak przeczytałem gdzieś, że większość pomiarów czasu odpowiedzi strony www mierzy w dużej mierze jaka działa infrastruktura interentu a nie jak działa strona www ale już nie pamiętam i nie moge tego znaleźć teraz)

No ale teraz jest ratunek, jest narzedzie - jest nadzieja!

jmh - teraz i ty możesz mówić, że umiesz robić pomiary!

Wydaje mi się, że najlepiej wejść tutaj : http://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/ i pooglądać sobie przykłady. Ja na początku trafiłem na tutorial z 2013 gdzie jakiś typ opisywał krok po kroku jak to konfigurować. Ponieważ otrzymywałem w wyniku czas wykonania 0 to na początku popadłem w depresję, że nawet jednej adnotacji nie umiem poprawnie dodać do kodu.

Na szczęście okazało się, że bezmyślnie kopiując kod używałem wersji 0.1 jmh. Po dodaniu zależności do 1.12 ku memu zachwytowi zaczęło to działać. A działać to oznacza, że udało się odpalić coś takiego :

@Benchmark
@BenchmarkMode(Mode.AverageTime)
public void boxed(){
   long numberOfElements = 1000000000;

   Long result = Stream
                .iterate(0L, i -> i + 1)
                .map(i->i*i)
                .limit(numberOfElements)
                .reduce(0L,Long::sum);


    System.out.println(result);
}

I dało końcu długo oczekiwany wynik :

# JMH 1.12 (released 81 days ago)
# VM version: JDK 1.8.0_91, VM 25.91-b14
# Warmup: 20 iterations, 1 s each
# Measurement: 20 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op

# Run progress: 0.00% complete, ETA 00:00:40
# Fork: 1 of 1
# Warmup Iteration   1: 3338615082255021824
13.917 s/op
# Warmup Iteration   2: 3338615082255021824
13.359 s/op
(...)
Iteration   1: 3338615082255021824
11.543 s/op
Iteration   2: 3338615082255021824
11.345 s/op
Iteration   3: 3338615082255021824
11.068 s/op
Iteration   4: 3338615082255021824
10.899 s/op
Iteration   5: 3338615082255021824
13.932 s/op
Iteration   6: 3338615082255021824
13.719 s/op
Iteration   7: 3338615082255021824
13.830 s/op
(...)

Result "boxed":
  13.295 ±(99.9%) 0.937 s/op [Average]
  (min, avg, max) = (10.899, 13.295, 14.231), stdev = 1.080
  CI (99.9%): [12.358, 14.233] (assumes normal distribution)


# Run complete. Total time: 00:08:42

Benchmark      Mode  Cnt   Score   Error  Units
JMHTest.boxed  avgt   20  13.295 ± 0.937   s/op

Jak komuś nie chce się czytać to wyszło średnio : 13 sekund

Po wsadzeniu kodu dla strumienia Longów :

Result "primitive":
  1.580 ±(99.9%) 0.009 s/op [Average]
  (min, avg, max) = (1.568, 1.580, 1.601), stdev = 0.011
  CI (99.9%): [1.571, 1.589] (assumes normal distribution)


# Run complete. Total time: 00:01:04

Benchmark          Mode  Cnt  Score   Error  Units
JMHTest.primitive  avgt   20  1.580 ± 0.009   s/op
Wyszło z 10 razy szybciej. Pytanie czy te pomiary są poprawne? Przede wszystkim nie zastosowałem się do zalecenia ze strony, żeby nie puszczać tego z IDE no i trudno mi stwierdzić czy ten kod jest na tyle "chytry", że JIT nic tam nie pousuwa. Wygląda na to, ze jmh jest przynajmniej wystarczająco sprytne by to jakoś odpowiednio odpalać tak, że iteracje rozgrzewania nie różnią się jakoś specjalnie czasem od iteracji wykonania to chyba ten czas 13s i 1.5s jest już po optymalizacji. Chyba. Temat jest ciekawy i wart zbadania.

I jeszcze sterta

Aby wpis był ciekawszy potrzebne są jakieś obrazki.

Tak wyglądał wykres zajętości pamięci pokazany przez visualvm dla przykładu ze strumieniem Łobiektów. A poniżej przykład dla strumienia prymitywnych Longów. Warto zauważyć, że operacje na prymitywach nie wywołały nawet pierdnięcia na procku.

Jak wyglądał pomiar? Zapauzowałem program na wczytywaniu znaku, odpaliłem visualvm i dalej program. Czy to jest dobrze? Nie wiem. Wykres na sprawozdanie wyszedł zgodny z przewidywaniami teoretycznymi.

Dlaczego nie ma zipa?

Jest gdzieś na necie prezentacja gościa z Oracla gdzie tłumaczy, że gdyby chcieli zrobić zipa dla wszystkich kombinacji różnych strumieni to wyszłoby tego z kilkanaście różnych wariacji. A gdyby chcieli dorzucić coś jeszcze to ilość wymknęła by się spod kontroli. Niby wszystkie te strumienie dziedziczą z czegoś co nazywa się BaseStream ale widocznie nie jest to takie abstrakcyjne aby można było napisać zip(BaseStream s1,BaseStream s2)

No i te 4 funkcje - chociaż wyglądają identycznie - to z punktu widzenia javy w kontekście typu mają ze sobą wspólnego NIC.

UnaryOperator<Integer> f1=i->i+1;
IntUnaryOperator f2=i->i+1;
LongUnaryOperator f3=i->i+1;
DoubleUnaryOperator f4=i->i+1;

Inne

Myślałem, że trochę przykozaczę i pokażę alternatywę bo w Scali są takie adnotacje

trait Function1[@specialized(scala.Int, scala.Long, scala.Float, scala.Double) -T1
I to według sprawi, że kompilator wygeneruje prymitywne wersje funkcji tak jak trzeba czyli gdy mamy :
class Specialization{
  val f:Function[Int,Int]=i=>i+1
}
To po dekompilacji faktycznie używa prymitywnych intów :
public final class mails.Specialization$$anonfun$1 extends scala.runtime.AbstractFunction1$mcII$sp implements scala.Serializable {
  public static final long serialVersionUID;
  public final int apply(int);
  public int apply$mcII$sp(int);
  public final java.lang.Object apply(java.lang.Object);
  public mails.Specialization$$anonfun$1(mails.Specialization);
}

Tej adnotacji nie ma w klasie Stream toteż w sumie tak trochę niepewny rezultatu odpaliłem ten kod :
val result=Stream
      .iterate(0L)(_+1)
      .map(i=>i*i)
      .take(1000000000)
      .reduce(_+_)

    println(result)
I musiałem przerwać bo youtube mi się zaczął przycinać. Temat ciekawy i do dalszego zgłębienia - być może ja robię coś źle (pewnie tak) a być może scala 2.12 która ma chodzić już na Javie 8 gdzieś tam pod spodem te IntStreamy ładnie wykorzysta i w przyszłości "samo z siebie" zadziała.

Próbowałem też z innym mniej popularnym językiem :

execute :: Int
execute = sum $ take 1000000000 $ map (+1) [1..]
Ale to już tak zagrzało kompa, że resztę testów zostawię na zimę...

Warsztat dla początkujących

Generalnie brak znajomości tego co się dzieje "pod spodem" trochę utrudnia programowanie. Także w duchu wspólnej edukacji meetup 5 lipca

Warsztaty Java 8 Streams - Wstęp - 5 lipca

Podsumowanie

To jest miejsce na jakieś mądre podsumowanie - jak mi cos kiedyś przyjdzie do głowy to może uzupełnię.

Brak komentarzy:

Prześlij komentarz