Czy jakiekolwiek kompilatory JVM JVM generują kod, który używa wektoryzowanych instrukcji zmiennoprzecinkowych?


95

Powiedzmy, że wąskim gardłem mojego programu w Javie są naprawdę ciasne pętle do obliczania szeregu wektorowych iloczynów skalarnych. Tak, sprofilowałem, tak, to wąskie gardło, tak, to jest znaczące, tak, taki właśnie jest algorytm, tak, uruchomiłem Proguard, aby zoptymalizować kod bajtowy itp.

Praca jest zasadniczo iloczynami skalarnymi. Tak jak w, mam dwa float[50]i muszę obliczyć sumę iloczynów parami. Wiem, że istnieją zestawy instrukcji procesora umożliwiające szybkie i masowe wykonywanie tego rodzaju operacji, takich jak SSE lub MMX.

Tak, prawdopodobnie mogę uzyskać do nich dostęp, pisząc natywny kod w JNI. Rozmowa JNI okazuje się dość droga.

Wiem, że nie możesz zagwarantować, co JIT skompiluje lub nie. Czy ktoś kiedykolwiek słyszał o kodzie generującym JIT, który używa tych instrukcji? a jeśli tak, to czy jest coś w kodzie Javy, co ułatwia jego kompilację w ten sposób?

Prawdopodobnie „nie”; warto zapytać.


4
Najłatwiejszym sposobem, aby się tego dowiedzieć, jest prawdopodobnie uzyskanie najnowocześniejszego JIT, jaki można znaleźć, i wypisanie wygenerowanego zestawu za pomocą -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:+LogCompilation. Będziesz potrzebować programu, który uruchamia metodę umożliwiającą wektoryzację wystarczająco dużo razy, aby była „gorąca”.
Louis Wasserman

1
Albo spójrz na źródło. download.java.net/openjdk/jdk7
Bill


3
Właściwie, zgodnie z tym blogiem , JNI może być dość szybki, jeśli jest używany "poprawnie".
ziggystar

2
Odpowiedni wpis na blogu na ten temat można znaleźć tutaj: psy-lob-saw.blogspot.com/2015/04/ ... z ogólnym przesłaniem, że wektoryzacja może się zdarzyć i się zdarza. Oprócz wektoryzacji określonych przypadków (Arrays.fill () / equals (char []) / arrayCopy) maszyna JVM automatycznie wektoryzuje się przy użyciu równoległości poziomu Superword. Odpowiedni kod znajduje się w superword.cpp, a dokument, na którym jest oparty, znajduje się tutaj: groups.csail.mit.edu/cag/slp/SLP-PLDI-2000.pdf
Nitsan Wakart

Odpowiedzi:


45

Zasadniczo chcesz, aby Twój kod działał szybciej. JNI jest odpowiedzią. Wiem, że powiedziałeś, że to nie zadziałało, ale pokażę ci, że się mylisz.

Oto Dot.java:

import java.nio.FloatBuffer;
import org.bytedeco.javacpp.*;
import org.bytedeco.javacpp.annotation.*;

@Platform(include = "Dot.h", compiler = "fastfpu")
public class Dot {
    static { Loader.load(); }

    static float[] a = new float[50], b = new float[50];
    static float dot() {
        float sum = 0;
        for (int i = 0; i < 50; i++) {
            sum += a[i]*b[i];
        }
        return sum;
    }
    static native @MemberGetter FloatPointer ac();
    static native @MemberGetter FloatPointer bc();
    static native @NoException float dotc();

    public static void main(String[] args) {
        FloatBuffer ab = ac().capacity(50).asBuffer();
        FloatBuffer bb = bc().capacity(50).asBuffer();

        for (int i = 0; i < 10000000; i++) {
            a[i%50] = b[i%50] = dot();
            float sum = dotc();
            ab.put(i%50, sum);
            bb.put(i%50, sum);
        }
        long t1 = System.nanoTime();
        for (int i = 0; i < 10000000; i++) {
            a[i%50] = b[i%50] = dot();
        }
        long t2 = System.nanoTime();
        for (int i = 0; i < 10000000; i++) {
            float sum = dotc();
            ab.put(i%50, sum);
            bb.put(i%50, sum);
        }
        long t3 = System.nanoTime();
        System.out.println("dot(): " + (t2 - t1)/10000000 + " ns");
        System.out.println("dotc(): "  + (t3 - t2)/10000000 + " ns");
    }
}

i Dot.h:

float ac[50], bc[50];

inline float dotc() {
    float sum = 0;
    for (int i = 0; i < 50; i++) {
        sum += ac[i]*bc[i];
    }
    return sum;
}

Możemy to skompilować i uruchomić z JavaCPP za pomocą tego polecenia:

$ java -jar javacpp.jar Dot.java -exec

Z procesorem Intel (R) Core (TM) i7-7700HQ @ 2,80 GHz, Fedora 30, GCC 9.1.1 i OpenJDK 8 lub 11, otrzymuję taki wynik:

dot(): 39 ns
dotc(): 16 ns

Lub około 2,4 raza szybciej. Musimy użyć bezpośrednich buforów NIO zamiast tablic, ale HotSpot może uzyskać dostęp do bezpośrednich buforów NIO tak szybko, jak tablice . Z drugiej strony ręczne rozwijanie pętli nie zapewnia w tym przypadku wymiernego wzrostu wydajności.


3
Czy korzystałeś z OpenJDK lub Oracle HotSpot? Wbrew powszechnemu przekonaniu to nie to samo.
Jonathan S. Fisher

@exabrial Oto, co „java -version” zwraca teraz na tym komputerze: wersja java „1.6.0_22” OpenJDK Runtime Environment (IcedTea6 1.10.6) (fedora-63.1.10.6.fc15-x86_64) OpenJDK 64-bitowa maszyna wirtualna serwera (kompilacja 20.0-b11, tryb mieszany)
Samuel Audet

1
Ta pętla prawdopodobnie ma zależność od pętli przenoszonej. Możesz uzyskać dalsze przyspieszenie, rozwijając pętlę dwa lub więcej razy.

3
@Oliv GCC wektoryzuje kod za pomocą SSE, tak, ale dla tak małych danych narzut wywołań JNI jest niestety zbyt duży.
Samuel Audet

2
Na moim A6-7310 z JDK 13 otrzymuję: dot (): 69 ns / dotc (): 95 ns. Java wygrywa!
Stefan Reich,

40

Aby odnieść się do części sceptycyzmu wyrażanego przez innych, sugeruję każdemu, kto chce udowodnić sobie lub innym, skorzystanie z następującej metody:

  • Utwórz projekt JMH
  • Napisz mały fragment matematyki z możliwością wektoryzacji.
  • Uruchom ich test porównawczy, przełączając między -XX: -UseSuperWord i -XX: + UseSuperWord (domyślnie)
  • Jeśli nie zaobserwujesz żadnej różnicy w wydajności, Twój kod prawdopodobnie nie został zwektoryzowany
  • Aby się upewnić, uruchom test porównawczy w taki sposób, aby wydrukował zestaw. W Linuksie możesz cieszyć się profilerem perfasm ('- prof perfasm'), rzucić okiem i zobaczyć, czy instrukcje, których oczekujesz, zostaną wygenerowane.

Przykład:

@Benchmark
@CompilerControl(CompilerControl.Mode.DONT_INLINE) //makes looking at assembly easier
public void inc() {
    for (int i=0;i<a.length;i++)
        a[i]++;// a is an int[], I benchmarked with size 32K
}

Wynik z flagą i bez niej (na ostatnim laptopie Haswell, Oracle JDK 8u60): -XX: + UseSuperWord: 475,073 ± 44,579 ns / op (nanosekundy na operację) -XX: -UseSuperWord: 3376,364 ± 233,211 ns / op

Zespół dla pętli gorącej wymaga trochę sformatowania i przyklejenia tutaj, ale oto fragment (hsdis.so nie formatuje niektórych instrukcji wektorowych AVX2, więc uruchomiłem z -XX: UseAVX = 1): -XX: + UseSuperWord (z '-prof perfasm: intelSyntax = true')

  9.15%   10.90%  │││ │↗    0x00007fc09d1ece60: vmovdqu xmm1,XMMWORD PTR [r10+r9*4+0x18]
 10.63%    9.78%  │││ ││    0x00007fc09d1ece67: vpaddd xmm1,xmm1,xmm0
 12.47%   12.67%  │││ ││    0x00007fc09d1ece6b: movsxd r11,r9d
  8.54%    7.82%  │││ ││    0x00007fc09d1ece6e: vmovdqu xmm2,XMMWORD PTR [r10+r11*4+0x28]
                  │││ ││                                                  ;*iaload
                  │││ ││                                                  ; - psy.lob.saw.VectorMath::inc@17 (line 45)
 10.68%   10.36%  │││ ││    0x00007fc09d1ece75: vmovdqu XMMWORD PTR [r10+r9*4+0x18],xmm1
 10.65%   10.44%  │││ ││    0x00007fc09d1ece7c: vpaddd xmm1,xmm2,xmm0
 10.11%   11.94%  │││ ││    0x00007fc09d1ece80: vmovdqu XMMWORD PTR [r10+r11*4+0x28],xmm1
                  │││ ││                                                  ;*iastore
                  │││ ││                                                  ; - psy.lob.saw.VectorMath::inc@20 (line 45)
 11.19%   12.65%  │││ ││    0x00007fc09d1ece87: add    r9d,0x8            ;*iinc
                  │││ ││                                                  ; - psy.lob.saw.VectorMath::inc@21 (line 44)
  8.38%    9.50%  │││ ││    0x00007fc09d1ece8b: cmp    r9d,ecx
                  │││ │╰    0x00007fc09d1ece8e: jl     0x00007fc09d1ece60  ;*if_icmpge

Baw się dobrze podczas szturmu na zamek!


1
Z tego samego artykułu: „wynik disasemblera JITed sugeruje, że w rzeczywistości nie jest on tak skuteczny pod względem wywoływania najbardziej optymalnych instrukcji SIMD i ich planowania. Szybkie wyszukiwanie w kodzie źródłowym kompilatora JVM JIT (Hotspot) sugeruje, że jest to spowodowane brak spakowanych kodów instrukcji SIMD. " Rejestry SSE są używane w trybie skalarnym.
Aleksandr Dubinsky

1
@AleksandrDubinsky niektóre przypadki są objęte gwarancją, inne nie. Masz konkretny przypadek, który Cię interesuje?
Nitsan Wakart

2
Odwróćmy pytanie i zapytajmy, czy JVM będzie autowektoryzować jakiekolwiek operacje arytmetyczne? Czy możesz podać przykład? Mam pętlę, którą ostatnio musiałem wyciągnąć i przepisać, używając funkcji wewnętrznych. Jednak zamiast mieć nadzieję na autowektoryzację, chciałbym zobaczyć wsparcie dla jawnej wektoryzacji / elementów wewnętrznych (podobnie jak agner.org/optimize/vectorclass.pdf ). Jeszcze lepiej byłoby napisać dobry backend w Javie dla Aparapi (chociaż kierownictwo tego projektu ma pewne złe cele). Czy pracujesz nad maszyną JVM?
Aleksandr Dubinsky

1
@AleksandrDubinsky Mam nadzieję, że rozszerzona odpowiedź pomoże, jeśli nie e-mail. Zwróć również uwagę, że „przepisanie przy użyciu elementów wewnętrznych” oznacza, że ​​zmieniłeś kod JVM, aby dodać nowe elementy wewnętrzne, czy to masz na myśli? Zgaduję, że miałeś na myśli zamianę kodu Java na wywołania natywnej implementacji przez JNI
Nitsan Wakart

1
Dziękuję Ci. To powinna być teraz oficjalna odpowiedź. Myślę, że należy usunąć odniesienie do artykułu, ponieważ jest on nieaktualny i nie wykazuje wektoryzacji.
Aleksandr Dubinsky

26

W wersjach HotSpot zaczynających się od Java 7u40, kompilator serwera zapewnia obsługę autowektoryzacji. Według JDK-6340864

Jednak wydaje się, że dotyczy to tylko „prostych pętli” - przynajmniej na razie. Na przykład gromadzenie tablicy nie może być jeszcze wektoryzowane JDK-7192383


W niektórych przypadkach wektoryzacja występuje również w JDK6, chociaż docelowy zestaw instrukcji SIMD nie jest tak szeroki.
Nitsan Wakart

3
Obsługa wektoryzacji kompilatora w HotSpot została ostatnio znacznie ulepszona (czerwiec 2017) dzięki wkładowi firmy Intel. Pod względem wydajności jeszcze niewydany jdk9 (b163 i nowsze) obecnie wygrywa z jdk8 z powodu poprawek błędów umożliwiających AVX2. Pętle muszą spełniać kilka ograniczeń, aby automatyczna wektoryzacja działała, np. Użycie: licznik int, stały przyrost licznika, jeden warunek zakończenia ze zmiennymi niezmiennymi pętli, treść pętli bez wywołań metod (?), Brak ręcznego rozwijania pętli! Szczegóły są dostępne na: cr.openjdk.java.net/~vlivanov/talks/…
Vedran

Obsługa wektoryzacji fused-multiple-add (FMA) nie wygląda obecnie dobrze (stan na czerwiec 2017): jest to wektoryzacja lub skalarna FMA (?). Jednak Oracle najwyraźniej właśnie zaakceptował wkład Intela w HotSpot, który umożliwia wektoryzację FMA przy użyciu AVX-512. Ku uciesze fanów auto-wektoryzacji i tych szczęśliwców, którzy mają dostęp do sprzętu AVX-512, może to (przy odrobinie szczęścia) pojawić się w jednej z następnych kompilacji EA z jdk9 (poza b175).
Vedran

Odnośnik do poprzedniego oświadczenia (RFR (M): 8181616: wektoryzacja FMA na x86): mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2017-June/ ...
Vedran

2
Mały test porównawczy demonstrujący przyspieszenie o współczynnik 4 na liczbach całkowitych poprzez wektoryzację pętli przy użyciu instrukcji AVX2
Vedran

6

Oto fajny artykuł o eksperymentowaniu z instrukcjami Javy i SIMD napisany przez mojego przyjaciela: http://prestodb.rocks/code/simd/

Jego ogólny wynik jest taki, że można oczekiwać, że JIT będzie używał niektórych operacji SSE w 1.8 (i trochę więcej w 1.9). Chociaż nie powinieneś oczekiwać wiele i musisz być ostrożny.


1
Byłoby pomocne, gdybyś podsumował kilka kluczowych spostrzeżeń dotyczących artykułu, do którego utworzyłeś łącze.
Aleksandr Dubinsky

4

Możesz napisać jądro OpenCl do obliczeń i uruchomić je z java http://www.jocl.org/ .

Kod można uruchomić na CPU i / lub GPU, a język OpenCL obsługuje również typy wektorowe, więc powinieneś być w stanie wyraźnie skorzystać z instrukcji np. SSE3 / 4.



3

Domyślam się, że napisałeś to pytanie, zanim dowiedziałeś się o netlib-java ;-) zapewnia dokładnie natywne API, którego potrzebujesz, z implementacjami zoptymalizowanymi pod kątem maszyny i nie ma żadnych kosztów na granicy natywnej dzięki przypinaniu pamięci.


1
Tak, dawno temu. Bardziej liczyłem na to, że usłyszę, że jest to automagicznie przetłumaczone na instrukcje wektoryzowane. Ale oczywiście nie jest tak trudno zrobić to ręcznie.
Sean Owen

-4

Nie wierzę w większość, jeśli jakiekolwiek maszyny wirtualne są kiedykolwiek wystarczająco inteligentne do tego rodzaju optymalizacji. Prawdę mówiąc, większość optymalizacji jest znacznie prostsza, na przykład przesuwanie zamiast mnożenia przy potędze dwóch. Projekt mono wprowadził własny wektor i inne metody z natywnymi podkładami, aby zwiększyć wydajność.


3
Obecnie żaden kompilator Java hotspot tego nie robi, ale nie jest to dużo trudniejsze niż rzeczy, które robią. Używają instrukcji SIMD do kopiowania wielu wartości tablic naraz. Musisz po prostu napisać więcej kodu dopasowywania wzorców i generowania kodu, co jest dość proste po wykonaniu niektórych rozwinięć pętli. Myślę, że ludzie w Sun po prostu się rozleniwili, ale wygląda na to, że teraz stanie się to w Oracle (yay Vladimir! To powinno bardzo pomóc naszemu kodowi!): Mail.openjdk.java.net/pipermail/hotspot-compiler-dev/ …
Christopher Manning,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.