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ć.
źródło
-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”.Odpowiedzi:
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:
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.
źródło
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:
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!
źródło
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
źródło
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.
źródło
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.
źródło
Spójrz na Porównanie wydajności między Javą i JNI w celu optymalnej implementacji obliczeniowych mikro-jąder . Pokazują, że kompilator serwera Java HotSpot VM obsługuje automatyczną wektoryzację przy użyciu paralelizmu poziomu super-słowa, który jest ograniczony do prostych przypadków równoległości wewnątrz pętli. Ten artykuł zawiera również wskazówki, czy rozmiar danych jest wystarczająco duży, aby uzasadnić wybranie trasy JNI.
źródło
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.
źródło
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ść.
źródło