Czy powinno się zwracać Optional?

1

Dostałem komentarz do PR co by zmienić return empty() na return null i użyć ofNullable wyżej.
Kto tu zwariował?

6

Czy powinno się zwracać Optional?

Tak

Dostałem komentarz do PR co by zmienić return empty() na return null i użyć ofNullable wyżej.
Kto tu zwariował?

Optional jest zje'any i nie jest serializowalny. Dlatego Oracle zaleca nie używanie go w API. Oczywiście prawie nikt nie używa wbudowanej w Javę serializacji więc nie jest to problemem, jednak Sonar i podobne narzedzia mogą mieć domyslnie regułę żeby Optionala nie używać w API

Najlepiej uzywać Option z Vavr, no ale taka decyzja jest pewnie poza twoim zasięgiem.
Jak denerwują cię zwariowani programiści Javy proponuję przekwalifikować się na programistę Kotlina lub Scali. Tam ludzie lepiej rozumieją ideą używania Option. (Nie mówiąc już o tym że w Kotlinie Option jest wbudowany w język jako ?)

4

Moje rozumienie użycia Optional jest takie, że po pierwsze informuje o możliwości wystąpienia wartości null, dopiero w drugim rzędzie pozwala coś z nią zrobić. Zwrócenie null uważam za raka.

1

W sumie zależy jak używasz, bo czym różni się

if (obj != null)

od

if(obj.isPresent())

Pominam rakowość metody isPresent. Ogólnie optional wymusi Ci obsługę nulla - więc jeśli tego potrzebujesz, to zwracaj optionala. Byle byś go nie używał w API.

8

Albo powiedz w PR , że nie - odeślij do nas jako szanowanego grona eksperckiego. Jeśli ten z PR to niestety ktoś z wyższą siłą przebicia - betonowy architekt - to szykuj sobie awans zewnętrzny.
(jak ktoś ma z problem Optionallami to dalej może być już tylko gorzej - pewnie gettery i settery też robicie, ew. lomboczycie).

1

Tak na serio to odpowiedź brzmi: to zależy. Zarówno VAVRowy Option, jak i Optional są cięższe niż normalny nullcheck (zwłaszcza korzystanie z filtrów, mapowania itp.) - dlatego w sytuacji gdy mocno walczysz o wydajność jest to jak najbardziej normalne.

W takim typowym kodzie crudowym liczy się bardziej czytelność kodu i wtedy Optional zwracać jak najbardziej można i warto, jeśli tylko obiekt lata sobie w obrębie VMki. Natomiast raczej głupim pomysłem jest korzystanie z Optionala jako pola w klasie.

3

Natomiast raczej głupim pomysłem jest korzystanie z Optionala jako pola w klasie.

2
dargenn napisał(a):

W sumie zależy jak używasz, bo czym różni się

if (obj != null)

od

if(obj.isPresent())

Pominam rakowość metody isPresent. Ogólnie optional wymusi Ci obsługę nulla - więc jeśli tego potrzebujesz, to zwracaj optionala. Byle byś go nie używał w API.

No różnica jest taka że jak masz Optionala to masz jawna informacje.
A od Javy 11 zdaje się jest metodą ifPresentOrElse która przyjmuje consumera dla obecnej wartości i runnable który jest uruchomiany jeśli Optional jest pusty.

0

Było jakieś uzasadnienie?

1
dargenn napisał(a):

W sumie zależy jak używasz, bo czym różni się

if (obj != null)

od

if(obj.isPresent())

Pominam rakowość metody isPresent. Ogólnie optional wymusi Ci obsługę nulla - więc jeśli tego potrzebujesz, to zwracaj optionala. Byle byś go nie używał w API.

No ale jak ktoś nauczy się używania Optional to if(obj.isPresent()) jest rzadkim przypadkiem. IMHO o wiele częściej używa się map orElse(T other) czy orElseGet(Supplier<? extends T> supplier) albo stream(). Przynajmniej z tego co pamiętam programowanie w Javie :P

4
  1. Luj ogłuszacz gościowi który dał ci taki komentarz na review
  2. Zwracanie Option/Optional/Either etc jest jak najbardziej ok
  3. Za samo pomyślenie o zrobieniu return null powinien być zakaz dotykania klawiatury. Jedyna sytuacja kiedy wolno coś takiego zrobić, to jak integrujesz się z jakimś API które inaczej nie umie i oczekuje nulla.
5

Jedyna sytuacja kiedy wolno coś takiego zrobić, to jak integrujesz się z jakimś API które inaczej nie umie i oczekuje nulla.

Nie no, jest jeszcze druga możliwość - zależy Ci na tym, aby Twój kod w Javie nie był powolniejszy 10x od ekwiwalentu w Rust/C/C++, a jedynie o 50%.
Optional powoduje alokację na stercie, która jeśli nie zostanie w szczególnym przypadku wyeliminowana przez JVM, który to przypadek zwykle zdarza się w mikrobenchmarkach a nie chce się zdarzać na produkcji, ma naprawdę spory narzut względem zwykłego przekazania referencji / nulla przez rejestr. I to narzut zarówno pamięciowy jak i CPU. Bodajże 16 B nagłówka obiektu, 8 B na właściwy wskaźnik lub null, 8 B wyrównania. Razem 32 bajty narzutu. Potem GC musi częściej sprzątać i płacisz jeszcze cyklami CPU.

W przypadku jak zwracasz wartość typu prostego a nie referencję jest jeszcze gorzej, bo Optional wymusza opakowanie wartości aby móc się odwołać przez referencję, czyli zamiast int będziesz mieć Integer. I cyk, kolejne 32 bajty na stercie robią papa. Razem 64 bajty aby zwrócić jakiegoś g***anego integera zajmującego 8 bajtów. Tylko dlatego, że chciałeś aby był opcjonalny i aby API było ładne. ;)

Dla porównania - w Rust Option na referencji kosztuje 0 bajtów, na typie prostym 8 bajtów (zakładając arch. 64 bitową), ale w pewnych sytuacjach nawet na typach prostych (np. typy NonZero***) 0 bajtów. I nigdy nie powoduje alokacji na stercie, w najgorszym przypadku idzie przez stos.

W Kotlinie zdaje się że też Option na referencji jest kompilowany pod spodem do ekwiwalentu na nullach, ale tego nie jestem pewien.

1

Chciałem zwrocic uwagę ze w Javie wcale nie trzeba robic softu dla rakiet balistycznych czy TGV zeby odczuc różnice w wydajności miedzy obydwoma rozwiązaniami (i ogólnie zobaczyć efekt jakichś mikrooptymalizacji).

Wystarczy, ze wczytujesz kilka milionow wierszy do raportu a w kazdym masz po kilkadziesiąt wywołań danej funkcji.

5

Co do powolności lub nie Optionala i jaki narzut daje.
Puściłem dla was benchmark - https://github.com/jarekratajski/-benchmarekPoczwarekOpt/blob/main/app/src/jmh/java/pl/setblack/optionalTest/Opt.java ( można cały projekt pobrać i opdalić przez `./gradlew jmh).

Kod jest obrzydliwie korzysta z Optional.get i Optional.isPresent, ale chodziło, aby przetestować jako "najbardziej wprost" alternatywę do null (jak najbliżej kodu z null).

    private Long randomNumber(Random rnd) {
        int i = rnd.nextInt(100);
        if (i > 10) {
            return  null;
        } else {
            return (long)i;
        }
    }

    private Optional<Long> randomNumberOpt(Random rnd) {
        int i = rnd.nextInt(100);
        if (i > 10) {
            return  Optional.empty();
        } else {
            return Optional.of((long)i);
        }
    }

//kod na nullach
    @Benchmark
    public long sumClassic() {
        final Random rnd = new Random(42);
        long sum = 0;
        for (int i=0; i<1_000_000;++i) {
            Long n = randomNumber(rnd);
            if (n != null) {
                sum+= n;
            }
        }
        assert sum == 549645;
        return sum;
    }
//kod na Optionallu
    @Benchmark
    public long sumOptional() {
        final Random rnd = new Random(42);
        long sum = 0;
        for (int i=0; i<1_000_000;++i) {
            Optional<Long> n = randomNumberOpt(rnd);
            if (n.isPresent()) {
                sum+= n.get();
            }
        }
        assert sum == 549645;
        return sum;
    }

Najpierw wyniki:

Benchmark                      Mode  Cnt    Score    Error  Units
optionalTest.Opt.sumClassic   thrpt    6  418.330 ±  7.261  ops/s
optionalTest.Opt.sumOptional  thrpt    6  413.619 ± 11.100  ops/s

W granicach błędu - takie same. Zaszło to o czy pisze @Krolik - w kodzie benchmarkowym jvm ładnie "eskejpuje" alokację na stercie (i używa stosu) - w efekcie nie ma różnicy. Obie implementacje generuja prawie ten sam kod (nie chciało mi się dumpować, ale strzelam, że różnią się instrukcją mov głównie i adresowaniem pośrednim).
Haczyk: taki ładny rezultat to mam tylko na:
tadam

# VM version: JDK 16.0.2, Java HotSpot(TM) 64-Bit Server VM, 16.0.2+7-jvmci-21.2-b08
# VM invoker: /home/jarek/programs/graalvm-ee-java16-21.2.0.1/bin/java
# VM options: -Dgraal.CompilerConfiguration=enterprise

Czyli trzeba graalvm i do tego jeszcze w wersji enterprise (olaboga...).

Użyjmy normalnego graala (community).


# JMH version: 1.29
# VM version: JDK 16.0.2, OpenJDK 64-Bit Server VM, 16.0.2+7-jvmci-21.2-b08
# VM invoker: /home/jarek/.sdkman/candidates/java/21.2.0.r16-grl/bin/java
# VM options: -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCIProduct -XX:-UnlockExperimentalVMOptions -XX:ThreadPriorityPolicy=1 -Dfile.encoding=UTF-8 -Djava.io.tmpdir=/home/jarek/dev/wisnie/benchmarks/javowe/app/build/tmp/jmh -Duser.country=US -Duser.language=en -Duser.variant

optionalTest.Opt.sumClassic   thrpt    6  397.427 ± 13.058  ops/s
optionalTest.Opt.sumOptional  thrpt    6  382.808 ±  9.249  ops/s

Tutaj też jest nieźle, ale optional wyszedł troszkę gorzej (nadal w granicach błędu takie same). Co ciekawe (ku mojemu zaskoczeniu) - escape analysis nie zadziałało i była alokacja na stercie. Czyli nawet w tak trywialnym benchmarkowym kodzie graal nie poradził sobie z eliminacją alokacji na stercie.sad frog
(czy są alokacje, czy nie - to ładnie widać jak się podłączymy do jvm benchmarku przez np. jvisualvm).

Tylko, że co z tego, skoro GC opiernicza te krótko żyjące optionale w pierdyliardach na sekunde. Może przesadziłem z pierdyliardami, ale widać, że dramatu nie ma.

To zobaczmy jeszcze - hotspota openjdk

# JMH version: 1.29
# VM version: JDK 17, OpenJDK 64-Bit Server VM, 17+35-2724
# VM invoker: /home/jarek/.sdkman/candidates/java/17-open/bin/java
# VM options: -Dfile.encoding=UTF-8 -Djava.io.tmpdir=/home/jarek/dev/wisnie/benchmarks/javowe/app/build/tmp/jmh -Duser.country=US -Duser.language=en -Duser.variant

optionalTest.Opt.sumClassic   thrpt    6  158.343 ± 5.865  ops/s
optionalTest.Opt.sumOptional  thrpt    6  155.063 ± 5.330  ops/s

Widać dużo gorsze (oba!!!) wyniki. W optionalu była alokacja (!!!), ale znowu: dramatu to raczej nie ma, bo w granicach błędu to samo.

Na koniec - bencharki to oczywiście tylko benchmarki. Oracle (wcześniej Sun) już kilkanaście lat temu odrobili lekcję i robią tak JVM, żeby dobrze wychodził w benchmarkach:
troll

W praktyce może być różnie, aczkolwiek stawiam hipotezę, że w typowych programach jakie robimy i które są bardziej io bound, ( nie męczą CPU), to ten optional naprawdę nie ma znaczenia.
Oczywiście może mieć znaczenie jak masz tablicę milionów Optionali lub coś podobnego.
Ciekawe byłoby przetestowanie jeszcze na różnych GC (np. nie generacyjnym) - ale nie mam aż tyle czasu w życiu na pierdoły.

Oczywiście wszystkie rezultaty były robione w pełni nieprofesjonalnie, nie dość, że na komputerze gdzie gra muzyka, 100 zakładek w chrome, to jeszcze na vmware itd. Olaboga. Dlatego daje źródła, żeby samemu sobie odpalić: https://github.com/jarekratajski/-benchmarekPoczwarekOpt
(ale ponieważ pi razy oko wiem co tam się dzieje to raczej względne wyniki wyjdą Ci podobne).

1

Disclaimer: nie chcę tutaj udowadniać, że nie warto używać Optionala, po prostu myślę że można by ten test poprawić.
Trochę zmieniłem testy i odpaliłem na JVM 11 bez dodatkowych opcji.

Środowisko:


# JMH version: 1.19
# VM version: JDK 11.0.11, VM 11.0.11+9-Ubuntu-0ubuntu2.20.04
# VM invoker: /usr/lib/jvm/java-11-openjdk-amd64/bin/java
# VM options: <none>

Wyniki:

Benchmark                  Mode  Cnt    Score   Error  Units
BenchTests.sumClassicJR   thrpt  200   90.690 ± 0.187  ops/s
BenchTests.sumClassicPL   thrpt  200  195.298 ± 6.729  ops/s
BenchTests.sumOptionalJR  thrpt  200   87.573 ± 0.322  ops/s
BenchTests.sumOptionalPL  thrpt  200  145.631 ± 0.600  ops/s

Co zmieniłem (suffiks: *PL) w stosunku do oryginalnych wersji (suffiks: *JR):

  • zmniejszyłem użycie Random.nextInt z testów (z 1 mln do 100 wywołań)
  • null/empty jest zwracany potencjalnie dla większej liczby przypadków
  • zamiast asercji użyłem Blackhole

Podsumowanie wyników:
Wg mnie oznaczają to że Random.nextInt dominuje czas wykonania a różnica m. Long a Optional<Long> jest dopiero widoczna gdy wszystko inne w teście jest zaniedbywalne.
Liczba wywołań nie zmieniła się, ale w mojej wersji czas wykonania jest znacząco (2x) mniejszy. Zakładam, że powoduje to właśnie Random.
W mojej wersji eliminacja Optional spowodowała skok wydajności jeśli dobrze liczę na poziomie 30%.
JMH znam słabo, opcje Javy żeby to zoptymalizować jeszcze mniej.

Mój kod:

plik 1:

// com.company.BenchTests

//kod na nullach
    @Benchmark
    public long sumClassicPL(Blackhole blackhole) {
        int size = 100;
        final SubjectClass sc = new SubjectClass(size);
        long sum = 0;
        for (int i=0; i<size;++i) {
          for (int j=0; j<size;++j) {
            for (int k=0; k<size;++k) {
		        Long v = sc.getValue(i, j, k);
		        if (v != null) {
		            sum += v;
		        }
		    }
		  } 
        }
        blackhole.consume(sum);
        return sum;
    }
//kod na Optionallu
    @Benchmark
    public long sumOptionalPL(Blackhole blackhole) {
        int size = 100;
        final SubjectClass sc = new SubjectClass(size);
        long sum = 0;
        for (int i=0; i<size;++i) {
          for (int j=0; j<size;++j) {
            for (int k=0; k<size;++k) {
		        Optional<Long> v = sc.getValueOpt(i, j, k);
		        if (v.isPresent()) {
		            sum += v.get();
		        }
		    }
		  } 
        }
        blackhole.consume(sum);
        return sum;
    }

plik 2:

// com.company.SubjectClass

public class SubjectClass {

    private final int[] testData;
    private final int sampleSize;
	
	public SubjectClass(int sampleSize) {
	   this.sampleSize = sampleSize;
	   this.testData = new int[sampleSize];
       final Random rnd = new Random(42);
       for(int i=0; i < testData.length; i++) {
         testData[i] = rnd.nextInt(sampleSize);
       }
	}
	
    public int getSampleSize() {
       return sampleSize;
    }
    
    public Long getValue(int x, int y, int z) {
        long result = doGetValue(x, y, z);
        if (result > 10) {
          return null;
        } else {
          return result;
        }  
    }

    public Optional<Long> getValueOpt(int x, int y, int z) {
        long result = doGetValue(x, y, z);
        if (result > 10) {
          return Optional.empty();
        } else {
          return Optional.of(result);
        }  
    }

    private long doGetValue(int x, int y, int z) {
        int a = testData[x];
        int b = testData[(a + y) % sampleSize];
        long c = testData[(b + z) % sampleSize];
        return c ^ b ^ a;
    }

}
1

@jarekr000000: @vpiotr: a sprawdzaliscie flame graph i co zajmuje najwiecej czasu? Bez tego nie wiadomo co wlaściwie mierzymy.
Moze w przypadku @jarekr000000 narzut optional to <1% czasu wywolania, wiec nawet jak jest 2x wolniejszy to praktycznie to nie wyjdzie w tym benchmarku.

Shameless plug, kiedys próbowalem zrobic podobne benchmarki dla różnych formatów i okazało się to całkiem tricky dla mnie z masą kwiatków - typu generowanie danych czy obiektów zajmuje 70% runtime.

4

@Fedaykin:
Po prostu mierzę konkretny scenariusz, w którym Optional jest, ale nawet niekoniecznie zajmuje najwięcej czasu. Ty niemniej sztucznie istotnie więcej niż w typowym programie, gdzie oprócz obliczania czegoś z optional (sumowanie to nie jest droga oparacja), robisz masę innych rzeczy. (u mnie Random miał symulować "inne rzeczy".) W sumie o to chodziło - nawet jak jest 2x wolniejszy to nie bardzo wyjdzie.

Tym niemniej zmieńmy ten kod, tak żeby już prawie nic poza Optionalem (i to "pełnym") nie było (tu nawet nie będzie czego zobaczyć we flame grafie).
W zasadzie to jest ten scenariusz typu tablica miliona optionali.

 private Long randomNumber(int n) {
        int i = n % 100;
        if (i > 98) {
            return null;
        } else {
            return (long) i;
        }
    }

    private Optional<Long> randomNumberOpt(int n) {
        int i = n % 100;
        if (i > 98) {
            return Optional.empty();
        } else {
            return Optional.of((long) i);
        }
    }

    @Benchmark
    public long sumClassic() {
        int k = 0;
        long sum = 0;
        for (int i = 0; i < 1_000_000; ++i) {
            Long n = randomNumber(k++);
            if (n != null) {
                sum += n;
            }
        }
        return sum;
    }

    @Benchmark
    public long sumOptional() {
        int k = 0;
        long sum = 0;
        for (int i = 0; i < 1_000_000; ++i) {
            Optional<Long> n = randomNumberOpt(k++);
            if (n.isPresent()) {
                sum += n.get();
            }
        }
        return sum;
    }

graal enterprise:

Benchmark                      Mode  Cnt     Score    Error  Units
optionalTest.Opt.sumClassic   thrpt    6  1216.536 ±  7.501  ops/s
optionalTest.Opt.sumOptional  thrpt    6  1215.374 ± 11.645  ops/s

graal community:

optionalTest.Opt.sumClassic   thrpt    6  748.317 ± 11.566  ops/s
optionalTest.Opt.sumOptional  thrpt    6  376.698 ±  5.545  ops/s

Jak widać - idealnie jak napisałeś 2x wolniejsze (bez enterprise). (Chociaż, jak się czepić to nadal to nie był sam Optional).

EDIT:
Dla lepszego obrazu dodaje jeszcze hotspoty...

# VM version: JDK 11.0.11, OpenJDK 64-Bit Server VM, 11.0.11+9
# VM invoker: /home/jarek/.sdkman/candidates/java/11.0.11.hs-adpt/bin/java
# VM options: -Dfile.encoding=UTF-8 -Djava.io.tmpdir=/home/jarek/dev/wisnie/benchmarks/javowe/app/build/tmp/jmh -Duser.country=US -Duser.language=en -Duser.variant
optionalTest.Opt.sumClassic   thrpt    6  623.655 ±  1.869  ops/s
optionalTest.Opt.sumOptional  thrpt    6  247.115 ± 24.290  ops/s
# VM version: JDK 17, OpenJDK 64-Bit Server VM, 17+35-2724
# VM invoker: /home/jarek/.sdkman/candidates/java/17-open/bin/java
# VM options: -Dfile.encoding=UTF-8 -Djava.io.tmpdir=/home/jarek/dev/wisnie/benchmarks/javowe/app/build/tmp/jmh -Duser.country=US -Duser.language=en -Duser.variant

optionalTest.Opt.sumClassic   thrpt    6  873.666 ± 5.492  ops/s
optionalTest.Opt.sumOptional  thrpt    6  352.608 ± 2.380  ops/s

Widać różnicę, ale widać też ładny progress na kolejnych wersjach jvm.

5

Fajnie, ze są benchmarki tylko z mojego punktu widzenia, to albo jest o co się bić z wydajnością i wtedy użycie Optional blednie na tle całych wiader ustępstw w clean code, dzieleniu na warstwy i zastąpieniu kolekcji tablicami, bo faktycznie trzeba się bić o każdy cykl procesora z jakiegoś tam powodu, albo mamy standardową aplikację działającą sobie na serwerze i wydajność w zupełnie nieistotnej części zależy od tego czy użyliśmy tej klasy, czy innej, bo narzut całego bałaganu z użyciem refleksji, komunikacją przez HTTP, odczyt i zapis danych do DB, czy od obsługa wyrażeń regularnych sprawia, że narzut iluś tam nanosekund nie ma żadnego realnego znaczenia. Gdyby wydajność była jedynym czynnikiem o który warto walczyć, to wszyscy siedzielibyśmy w asm'ie. Nawet jeżeli jest sens walczyć o wydajność, to zwykle ten miliard wywołań na sekundę można opakować w coś (klasa, moduł, metoda) z napisem "DRAGONS AHEAD, DO NOT ENTER` a efekt tego miliona wywołań zapakować w optionala jeżeli ma to sens. Wtedy mamy prawie maksymalną wydajność tam gdzie trzeba i "ładnie" tam gdzie na tej wydajności nam nie zależy tak bardzo.

Zupełnie osobna sprawa, to pytanie czy jeżeli mamy zadanie w którym wydajność jest naprawdę ważne powinniśmy używać do jego obsługi języka, który od 25 lat twierdzi, że już za chwilę będzie szybszy od C++ i jak to kolejne wynalazki pozwolą uzyskać nie wiadomo jak wielkie przyśpieszenie, o którym później mówi się już jakoś mniej.

5

@piotrpo: dokładnie.
Najgorsze, że raz na jakiś czas ktoś w internecie palnie o wydajności coś co nawet w jakimś kontekście (milion na sekunde) jest prawdą i potem przez lata trzeba walczyć, bo przyłażą uberarchitekci i Ci mówią, że niewydajne (czytali), albo nowi programiści i tak samo trzeba tłuc od razu koszmarne nawyki. (tylko midom zwykle zwisa).

Najgorsze, jak wskutek postępu technicznego taka sprawa sprzed dziesieciu lat jest już nieaktualna, ale duchy wydajności nadal straszą. Swego czasu mój zespół przepisywał całe kawały kodu pod dyktando germańskich architektów, co gdzieś coś wyczytali - mimo, że wiedzieliśmy i umieliśmy pokazać, że bezedura....Benchmarki, benchmarkami, ale 2 wpisy blogowe javowych góru sprzed pięciu lat nie mogą być błędne.

Dlatego jednak te jmh publikuję, żeby chociaż te rzeczy które już stały się bzdurami odsiać - Optional ma niezłą szansę być zupełnie odsiany za jakiś czas nawet w tych miliardach na sekundę.

Zabawne będzie jak ktoś kiedyś spojrzy na te moje wypociny i powie: o pacz - typy pisze jaki Optional wolny ( a moja teza była inna).
Żeby było śmieszniej to pracuje teraz przy wydajności na cały etat, operniczam taki funkcyjny kod, że Optional to przy tym pikuś. I jakoś działa.
(ale its complicated - nie chce tutaj pisać i kłamać, że funkcyjny kod jest szybszy, po prostu nawet tam, gdzie wydajność się liczy i opitalamy jakieś 1000 req na sekunde, nadal jest miejsce na "pure code").

0

@jarekr000000: O, to jest właśnie bardzo interesujące. Wygląda na to, że Optional vs null może być co najmniej 2x wolniejszy (a łatwo sobie wyobrazić przypadki gdzie może wymuszać nawet większe spowolnienie całości - choćby ze względu na wymóg pełnych obiektów w środku). Dzięki za porównanie z JDK 11, zaskakująca różnica pomiędzy wersjami.

Generalnie zgadzam się, że ilość aplikacji dla których to ma znaczenie jest relatywnie mała - i co więcej, cały czas się zmniejsza (nowoczesne komputery i sama Java są bardzo szybkie) - to jednak istnieją i będą istnieć dziedziny gdzie podobne optymalizacje mają czasami sens - procesowanie dużych wolumenów danych, bazy danych, procesowanie message'y, systemy rozproszone, image processing, AI, HPC etc. generalnie wszystkie dziedziny gdzie poza programowaniem znaczenie ma jeszcze informatyka albo skala.

Zupełnie osobna sprawa, to pytanie czy jeżeli mamy zadanie w którym wydajność jest naprawdę ważne powinniśmy używać do jego obsługi języka, który od 25 lat twierdzi, że już za chwilę będzie szybszy od C++ i jak to kolejne wynalazki pozwolą uzyskać nie wiadomo jak wielkie przyśpieszenie, o którym później mówi się już jakoś mniej.

Tylko, że Java faktycznie może być całkiem szybka - inna sprawa, że trzeba odpowiednio się dostosować pisząc kod - vide narzekanie @Krolik że niby Java a i tak trzeba żonglować bajtami z lewej na prawą i ręcznie zwalniać pamięć.

5

A na końcu wchodzi Królik cały na biało i wyciąga Rusta.

Kod

Java

@Fork(1)
@Warmup(iterations = 2)
@Measurement(iterations = 5)
public class OptionBenchmark {

    private Long randomNumber(int n) {
        int i = n % 100;
        if (i > 98) {
            return null;
        } else {
            return (long) i;
        }
    }

    private Optional<Long> randomNumberOpt(int n) {
        int i = n % 100;
        if (i > 98) {
            return Optional.empty();
        } else {
            return Optional.of((long) i);
        }
    }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.MICROSECONDS)
    public long sumClassic() {
        int k = 0;
        long sum = 0;
        for (int i = 0; i < 1_000_000; ++i) {
            Long n = randomNumber(k++);
            if (n != null) {
                sum += n;
            }
        }
        return sum;
    }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.MICROSECONDS)
    public long sumOptional() {
        int k = 0;
        long sum = 0;
        for (int i = 0; i < 1_000_000; ++i) {
            Optional<Long> n = randomNumberOpt(k++);
            if (n.isPresent()) {
                sum += n.get();
            }
        }
        return sum;
    }

}

Rust

fn get_optional_int(n: u64) -> Option<u64> {
    let i = n % 100;
    if i > 98 { None } else { Some(i) }
}

pub fn sum_optional() -> u64 {
    let mut sum = 0;
    for i in 0..1_000_000 {
        let value = get_optional_int(i);
        if let Some(k) = value {
            sum += k;
        }
    }
    sum
}

fn bench_optional(c: &mut Criterion) {
    c.bench_function("sum_optional", |b| b.iter(|| sum_optional()));
}

Wyniki

Java 11

# JMH version: 1.31
# VM version: JDK 11, Java HotSpot(TM) 64-Bit Server VM, 11+28
# VM invoker: /usr/lib/jvm/jdk-11/bin/java
# VM options: -javaagent:/opt/idea-IC-203.7148.57/lib/idea_rt.jar=44857:/opt/idea-IC-203.7148.57/bin -Dfile.encoding=UTF-8
...

Benchmark                    Mode  Cnt     Score     Error  Units
OptionBenchmark.sumClassic   avgt    5  2660,111 ±  36,878  us/op
OptionBenchmark.sumOptional  avgt    5  3972,721 ± 166,431  us/op

Java 17

# JMH version: 1.31
# VM version: JDK 17-ea, OpenJDK 64-Bit Server VM, 17-ea+19-Ubuntu-1ubuntu1
# VM invoker: /usr/lib/jvm/java-17-openjdk-amd64/bin/java
# VM options: -javaagent:/opt/idea-IC-203.7148.57/lib/idea_rt.jar=42107:/opt/idea-IC-203.7148.57/bin -Dfile.encoding=UTF-8
...
Benchmark                    Mode  Cnt     Score     Error  Units
OptionBenchmark.sumClassic   avgt    5  1239,155 ±  39,432  us/op
OptionBenchmark.sumOptional  avgt    5  3110,494 ± 160,103  us/op

Rust 1.55 bez SSE/AVX

sum_optional            time:   [658.74 us 661.64 us 664.74 us]                         
Found 6 outliers among 100 measurements (6.00%)
  1 (1.00%) low mild
  4 (4.00%) high mild
  1 (1.00%) high severe

Żeby nie było posądzeń o jakieś nieczyste tricki kompilatora, że np. niby zorientował się że to bezużyteczny benchmark i uprościł kod zanadto, wklejam disassembly z perfa:

       │    btree_benchmark::sum_optional:                                                                                                                                                                                                                                                                    
  0,00 │      mov    $0x1,%r10d                                                                                                                                                                                                                                                                               
       │      xor    %r8d,%r8d                                                                                                                                                                                                                                                                                
       │      mov    $0xf4240,%r9d                                                                                                                                                                                                                                                                            
       │      xor    %edi,%edi                                                                                                                                                                                                                                                                                
       │      xor    %eax,%eax                                                                                                                                                                                                                                                                                
       │      cs     nopw 0x0(%rax,%rax,1)                                                                                                                                                                                                                                                                    
       │      nop                                                                                                                                                                                                                                                                                             
  8,18 │20:   mov    %edi,%edx                                                                                                                                                                                                                                                                                
  3,05 │      imul   $0x51eb851f,%rdx,%rdx                                                                                                                                                                                                                                                                    
  7,62 │      shr    $0x25,%rdx                                                                                                                                                                                                                                                                               
  3,24 │      imul   $0x64,%edx,%edx                                                                                                                                                                                                                                                                          
  8,17 │      mov    %r10d,%esi                                                                                                                                                                                                                                                                               
  3,37 │      sub    %edx,%esi                                                                                                                                                                                                                                                                                
  7,67 │      mov    %edi,%ecx                                                                                                                                                                                                                                                                                
  3,26 │      sub    %edx,%ecx                                                                                                                                                                                                                                                                                
  8,10 │      cmp    $0x63,%ecx                                                                                                                                                                                                                                                                              
  3,48 │      cmovae %r8d,%ecx                                                                                                                                                                                                                                                                                
  7,93 │      add    %rax,%rcx                                                                                                                                                                                                                                                                                
  3,28 │      cmp    $0x63,%esi                                                                                                                                                                                                                                                                               
  7,74 │      cmovae %r8d,%esi                                                                                                                                                                                                                                                                                
  3,15 │      mov    %rsi,%rax                                                                                                                                                                                                                                                                                
  8,37 │      add    %rcx,%rax                                                                                                                                                                                                                                                                                
  2,95 │      add    $0x2,%r10d                                                                                                                                                                                                                                                                               
  7,40 │      add    $0x2,%edi                                                                                                                                                                                                                                                                                
       │      add    $0xfffffffffffffffe,%r9                                                                                                                                                                                                                                                                  
  3,06 │    ↑ jne    20                                                                                                                                                                                                                                                                                       
       │    ← ret                         

Rust 1.55 skompilowany pod konkretne CPU:

Po skompilowaniu na mój procesor, tj. dodatkowo z flagą RUSTFLAGS="-C target-cpu=native", jest jeszcze lepiej:

sum_optional            time:   [437.86 us 438.87 us 439.97 us]                         
                        change: [-34.978% -34.643% -34.315%] (p = 0.00 < 0.05)
                        Performance has improved.

A kod wygląda teraz tak:

  0,21 │ 70:   vpaddd       0x10(%rsp),%xmm1,%xmm3                                                                                                                                                                                                                                                            
  3,29 │       vpaddd       (%rsp),%xmm1,%xmm4                                                                                                                                                                                                                                                                
  0,03 │       vpaddd       %xmm1,%xmm13,%xmm2                                                                                                                                                                                                                                                                
  0,49 │       vpshufd      $0xf5,%xmm1,%xmm6                                                                                                                                                                                                                                                                 
  0,23 │       vpmuludq     %xmm5,%xmm6,%xmm6                                                                                                                                                                                                                                                                 
  3,38 │       vpmuludq     %xmm5,%xmm1,%xmm8                                                                                                                                                                                                                                                                 
  0,04 │       vpshufd      $0xf5,%xmm8,%xmm7                                                                                                                                                                                                                                                                 
  0,63 │       vpblendd     $0xa,%xmm6,%xmm7,%xmm8                                                                                                                                                                                                                                                            
  0,23 │       vpshufd      $0xf5,%xmm3,%xmm7                                                                                                                                                                                                                                                                 
  3,36 │       vpmuludq     %xmm5,%xmm7,%xmm7                                                                                                                                                                                                                                                                 
  0,03 │       vpmuludq     %xmm5,%xmm3,%xmm6                                                                                                                                                                                                                                                                 
  0,74 │       vpshufd      $0xf5,%xmm6,%xmm6                                                                                                                                                                                                                                                                 
  0,47 │       vpblendd     $0xa,%xmm7,%xmm6,%xmm6                                                                                                                                                                                                                                                            
  5,96 │       vpsrld       $0x5,%xmm6,%xmm6                                                                                                                                                                                                                                                                  
  4,84 │       vpmulld      %xmm15,%xmm6,%xmm6                                                                                                                                                                                                                                                                
  6,80 │       vpsubd       %xmm6,%xmm3,%xmm3                                                                                                                                                                                                                                                                 
  1,84 │       vpshufd      $0xf5,%xmm4,%xmm6                                                                                                                                                                                                                                                                 
  0,01 │       vpmuludq     %xmm5,%xmm6,%xmm6                                                                                                                                                                                                                                                                 
  0,22 │       vpmuludq     %xmm5,%xmm4,%xmm7                                                                                                                                                                                                                                                                 
  2,12 │       vpshufd      $0xf5,%xmm7,%xmm7                                                                                                                                                                                                                                                                 
  2,11 │       vpblendd     $0xa,%xmm6,%xmm7,%xmm6                                                                                                                                                                                                                                                            
  0,02 │       vpsrld       $0x5,%xmm6,%xmm6                                                                                                                                                                                                                                                                  
  3,80 │       vpmulld      %xmm15,%xmm6,%xmm6                                                                                                                                                                                                                                                                
  6,31 │       vpsubd       %xmm6,%xmm4,%xmm4                                                                                                                                                                                                                                                                 
  0,03 │       vpshufd      $0xf5,%xmm2,%xmm6                                                                                                                                                                                                                                                                 
  0,13 │       vpmuludq     %xmm5,%xmm6,%xmm6                                                                                                                                                                                                                                                                 
  1,24 │       vpmuludq     %xmm5,%xmm2,%xmm7                                                                                                                                                                                                                                                                 
  2,68 │       vpshufd      $0xf5,%xmm7,%xmm7                                                                                                                                                                                                                                                                 
  0,02 │       vpblendd     $0xa,%xmm6,%xmm7,%xmm6                                                                                                                                                                                                                                                            
  0,13 │       vpsrld       $0x5,%xmm8,%xmm7                                                                                                                                                                                                                                                                  
  4,32 │       vpmulld      %xmm15,%xmm7,%xmm7                                                                                                                                                                                                                                                                
  0,02 │       vpsrld       $0x5,%xmm6,%xmm6                                                                                                                                                                                                                                                                  
  1,96 │       vpmulld      %xmm15,%xmm6,%xmm6                                                                                                                                                                                                                                                                
  2,70 │       vpsubd       %xmm7,%xmm1,%xmm7                                                                                                                                                                                                                                                                 
  2,95 │       vpsubd       %xmm6,%xmm2,%xmm2                                                                                                                                                                                                                                                                 
  0,14 │       vpminud      %xmm12,%xmm7,%xmm6                                                                                                                                                                                                                                                                
  0,87 │       vpcmpeqd     %xmm6,%xmm7,%xmm6                                                                                                                                                                                                                                                                 
  1,91 │       vpand        %xmm7,%xmm6,%xmm6                                                                                                                                                                                                                                                                 
  1,12 │       vpminud      %xmm12,%xmm3,%xmm7                                                                                                                                                                                                                                                                
  0,05 │       vpcmpeqd     %xmm7,%xmm3,%xmm7                                                                                                                                                                                                                                                                 
  0,87 │       vpand        %xmm3,%xmm7,%xmm3                                                                                                                                                                                                                                                                 
  2,24 │       vpminud      %xmm12,%xmm4,%xmm7                                                                                                                                                                                                                                                                
  1,88 │       vpcmpeqd     %xmm7,%xmm4,%xmm7                                                                                                                                                                                                                                                                 
  0,16 │       vpand        %xmm4,%xmm7,%xmm4                                                                                                                                                                                                                                                                 
  1,79 │       vpminud      %xmm12,%xmm2,%xmm7                                                                                                                                                                                                                                                                
  2,89 │       vpcmpeqd     %xmm7,%xmm2,%xmm7                                                                                                                                                                                                                                                                 
  1,05 │       vpand        %xmm2,%xmm7,%xmm2                                                                                                                                                                                                                                                                 
  0,20 │       vpmovzxdq    %xmm6,%ymm6                                                                                                                                                                                                                                                                       
  1,79 │       vpaddq       %ymm6,%ymm0,%ymm0                                                                                                                                                                                                                                                                 
  2,72 │       vpmovzxdq    %xmm3,%ymm3                                                                                                                                                                                                                                                                       
  2,00 │       vpaddq       %ymm3,%ymm9,%ymm9                                                                                                                                                                                                                                                                 
  0,42 │       vpmovzxdq    %xmm4,%ymm3                                                                                                                                                                                                                                                                       
  3,86 │       vpaddq       %ymm3,%ymm10,%ymm10                                                                                                                                                                                                                                                               
  1,49 │       vpmovzxdq    %xmm2,%ymm2                                                                                                                                                                                                                                                                       
  8,70 │       vpaddq       %ymm2,%ymm11,%ymm11                                                                                                                                                                                                                                                               
  0,06 │       vpaddd       %xmm1,%xmm14,%xmm1                                                                                                                                                                                                                                                                
       │       add          $0xfffffffffffffff0,%rax                                                                                                                                                                                                                                                          
  0,45 │     ↑ jne          70                                             

Tylko, że Java faktycznie może być całkiem szybka

:D

Podsumowując, widzę, że moje wyniki dla JDK 17 po przeliczeniu na ops/s zgadzają się dość dobrze z wynikami które uzyskał @jarekr000000, więc wnioskuję, że mamy sprzęt o podobnej wydajności. Stąd wychodzi mi, że jedynie Graal Enterprise w miarę zbliża się do Rusta skompilowanego opcjami domyślnymi, ale nadal jest daleko za kodem natywnym skompilowanym pod nowocześniejszy zestaw instrukcji. Natomiast Hotspoty... no cóż, długa droga przed nimi.

0

@Krolik: meh, w wersji rustowej zwróciłeś optionala na stosie, a nie na stercie (semantycznie przynajmniej, pomijając escape analysis). cziter :]

3

Zrobiłem jeszcze jeden ciakwy eksperyment - mianowicie, co się dzieje, jeśli metoda zwracająca Optional nie zostanie włączona jako inline.
Jest to przypadek o tyle bardziej realistyczny, że w prawdziwym kodzie, gdzie mamy długie łańcuchy wywołań, jeszcze do tego dojdzie pośredniość / dispatch dynamiczny, to inline może się nie udać. I wtedy hotspot nie wyeliminuje tego Optionala.

Wymusiłem na Javie -XX:-Inline a w wersji Rustowej dałem #[inline(never)].
Dodałem też trzecią implementację, w której nie ma boxingu, tj zmieniłem Long na long i zamiast nulla zwracam 0.

    private long randomNumberNoBoxing(int n) {
        int i = n % 100;
        if (i > 98) {
            return 0;
        } else {
            return i;
        }
    }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.MICROSECONDS)
    public long sumNoBoxing() {
        int k = 0;
        long sum = 0;
        for (int i = 0; i < 1_000_000; ++i) {
            long n = randomNumberNoBoxing(k++);
            sum += n;
        }
        return sum;
    }

Dopisałem też dwa warianty sumowania w Rust:

#[inline(never)]
fn get_optional_non_zero(n: u64) -> Option<NonZeroU64> {
    let i = n % 100;
    if i > 98 { None } else { NonZeroU64::new(i) }
}

pub fn sum_optional_non_zero() -> u64 {
    let mut sum = 0;
    for i in 0..1_000_000 {
        let value = get_optional_non_zero(i);
        if let Some(k) = value {
            sum += k.get();
        }
    }
    sum
}

#[inline(never)]
fn get_int(n: u64) -> u64 {
    let i = n % 100;
    if i > 98 { 0 } else { i }
}

pub fn sum_classic() -> u64 {
    let mut sum = 0;
    for i in 0..1_000_000 {
        sum += get_int(i);
    }
    sum
}

Wyniki:

Benchmark                    Mode  Cnt      Score     Error  Units
OptionBenchmark.sumNoBoxing  avgt    5   2757,224 ±  20,870  us/op
OptionBenchmark.sumClassic   avgt    5   5286,291 ±  27,298  us/op
OptionBenchmark.sumOptional  avgt    5  16297,475 ± 436,858  us/op

sum_classic             time:   [1.4416 ms 1.4453 ms 1.4494 ms]   
sum_optional_non_zero   time:   [1.4379 ms 1.4422 ms 1.4460 ms]                                   
sum_optional            time:   [2.1216 ms 2.1304 ms 2.1411 ms]  
                        

To jest JDK 17 - widzimy, że narzut samego Optional w tej sytuacji jest już ponad 3x, a jak się dorzuci jeszcze narzut na boxing to już robi się prawie 6x.
Tymczasem w Rust ten narzut jest albo dokładnie zero, jeśli jesteśmy w stanie zamapować jedną specjaną wartość na None (tutaj 0 -> None), natomiast w ogólnym przypadku narzut jest na poziomie raptem 50%. Przy czym w Rust optional jest i tak szybszy niż najprostszy możliwy kod w Javie, bez optionala i bez boxingu.

BTW, teraz jak mam więcej wariantów w Rust, powtórzyłem eksperyment z włączonym inline i jest ciekawa niespodzianka.
Ustawienia domyślne kompilatora:

sum_classic             time:   [723.90 us 725.35 us 727.25 us]                        
sum_optional_non_zero   time:   [743.59 us 745.04 us 746.74 us]                                  
sum_optional            time:   [657.46 us 659.82 us 662.66 us]                          // WAT?! optionale są szybsze :D :D :D

Ustawienia pod procesor:

sum_classic             time:   [435.15 us 436.87 us 438.92 us]           
sum_optional_non_zero   time:   [436.54 us 437.31 us 438.20 us]                                  
sum_optional            time:   [437.15 us 438.25 us 439.43 us]                         
2
Lwojtow napisał(a):

Dostałem komentarz do PR

To zapytaj osoby dającej komentarz o co mu chodzi, czemu, z jakiego powodu, czy zwariował, czy ty nie rozumiesz. Kode rewju działa w obie strony.

0

@Patryk Maleszko: Dokładnie. Plagą na review bywa wrzucanie komentarzy,. bo się komuś coś wydaje, albo gdzieś coś usłyszał, ale nie zrozumiał do końca i stało się to dla niego prawdą wiary, za którą jest gotowy oddać życie swojej klawiatury.

2

Powinno albo nie powinno- to zależy. Review to dobra okazja do dyskusji oraz uzgadniania nowych konwencji. Wiem że to może dla kogoś brzmieć jak herezja ale ja jestem w stanie zrozumieć sytuacje gdzie ktoś przekłada spójność kodu nad arbitralnie dodawane własnych koncepcji przez poszczególnych programistów. Czasem ofiarą padają właśnie dobre koncepcje.

Reasumując- jeśli to było dodane bez żadnej komunikacji, bez zachęcenia to stosowania takiego podejścia, to ja w zasadzie byłbym po stronie tego kto zostawił Ci ten komentarz (chociaż moim zdaniem sam również mógłby zaczac dyskusję).

2

@Aventus: Jeżeli wchodzisz w jakiś projekt gdzie ludzie ustalili sobie jakieś tam zasady, to jasne, że spójność kodu jest powodem dla którego można odejść od "lepszych" praktyk. Ale w taki wypadku wypada dodać w komentarzu dlaczego ktoś chce tak a nie inaczej, albo można spokojnie przegadać problem:

0

@piotrpo: no tak, przecież napisałem że to dobra okazja do dyskusji.

3

Spójność to sposób na obudzenie się któregoś dnia z kodem tak przestarzałym, że już nikt nie będzie chciał/umiał go tykać.
Oczywiście - it depends. Jak mam system, który ewidentnie dogorywa i robi się w nim zmianę raz na kwartał - to faktycznie nie wprowadzam tam nowych koncepcji.
Ale jak mówimy o systemie, który jest ciągle dewelopowany przez kilku ludzi to inna bajka. Stagnacja to degeneracja.

1

Spójność to sposób na obudzenie się któregoś dnia z kodem tak przestarzałym, że już nikt nie będzie chciał/umiał go tykać.

Spójność nie oznacza braku postępu, rzecz w tym żeby nowe idee wprowadzać jako konwencje/standardy, i starać się w miarę szybko je wprowadzić wszędzie w celu właśnie spójności. Spójność wcale nie musi być ekstremum po jednej stronie spektrum. Rzecz w tym żeby wprowadzać rzeczy z głową. Jak zawsze powtarzam kolegom w pracy- balance is everything. Gdyby ślepo iść w imię tego co napisałeś, to należałoby wprowadzać natychmiast, miejscowo, każdą "dobrą" zmianę jaką proponuje programista- ciekawe czy taki chaotyczny kod będzie lepszy od tego "przestarzałego" (cudzysłów zastosowany umyślnie).

Większość doświadczonych programistów z głową ma "nowe" pomysły zmian, szczególnie kiedy zaczynają pracę w nowym miejscu. Te pomysły biorą się z faktycznego znania rożnych nowinek, z doświadczenia, ale również przyzwyczajenia do konkretnych konwencji, a czasem z błędnego przekonania że to z czym pracowali do tej pory jest jedynym słusznym sposobem programowania. Nie ma w tym nic niezwykłego. Rzecz w tym że na poziomie kodu i zespołu ogólnie trzeba odpowiednio zarządzać i lawirować pomiędzy wprowadzaniem odpowiednich zmian, a zachowaniem pewnej spójności żeby nie zrobiło się nam architektoniczne i konwencyjne spaghetti. Raz jeszcze- balance is everything.

Ale jak mówimy o systemie, który jest ciągle dewelopowany przez kilku ludzi to inna bajka. Stagnacja to degeneracja.

To Ty stawiasz znak równości między spójnością a stagnacją. Operujesz na ekstremach na podstawie błędnych interpretacji, co jest bez sensu.

1 użytkowników online, w tym zalogowanych: 0, gości: 1