I compilatori JIT di JVM generano codice che utilizza istruzioni in virgola mobile vettorializzate?


95

Diciamo che il collo di bottiglia del mio programma Java è davvero un ciclo stretto per calcolare un mucchio di prodotti a punti vettoriali. Sì, ho profilato, sì, è il collo di bottiglia, sì è significativo, sì è proprio così che è l'algoritmo, sì, ho eseguito Proguard per ottimizzare il codice byte, ecc.

Il lavoro è, essenzialmente, prodotti puntuali. Come in, ne ho due float[50]e ho bisogno di calcolare la somma dei prodotti a coppie. So che esistono set di istruzioni del processore per eseguire questo tipo di operazioni rapidamente e in blocco, come SSE o MMX.

Sì, probabilmente posso accedervi scrivendo del codice nativo in JNI. La chiamata JNI risulta essere piuttosto costosa.

So che non puoi garantire cosa compilerà o meno un JIT. Qualcuno ha mai sentito parlare di un codice che genera JIT che utilizza queste istruzioni? e se è così, c'è qualcosa nel codice Java che aiuta a renderlo compilabile in questo modo?

Probabilmente un "no"; vale la pena chiedere.


4
Il modo più semplice per scoprirlo è probabilmente quello di ottenere il JIT più moderno che puoi trovare e farlo stampare con l'assembly generato -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:+LogCompilation. Avrai bisogno di un programma che esegua il metodo vettorializzabile abbastanza volte da renderlo "caldo".
Louis Wasserman

1
Oppure dai un'occhiata alla fonte. download.java.net/openjdk/jdk7
Bill


3
In realtà, secondo questo blog , JNI può essere piuttosto veloce se usato "correttamente".
ziggystar

2
Un post sul blog pertinente su questo può essere trovato qui: psy-lob-saw.blogspot.com/2015/04/… con il messaggio generale che la vettorializzazione può accadere, e accade. Oltre a vettorizzare casi specifici (Arrays.fill () / equals (char []) / arrayCopy), la JVM si auto-vettorizza utilizzando la parallelizzazione a livello di superparola. Il codice pertinente è in superword.cpp e il documento su cui si basa è qui: groups.csail.mit.edu/cag/slp/SLP-PLDI-2000.pdf
Nitsan Wakart

Risposte:


44

Quindi, fondamentalmente, vuoi che il tuo codice funzioni più velocemente. JNI è la risposta. So che hai detto che non ha funzionato per te, ma lascia che ti dimostri che ti sbagli.

Ecco 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");
    }
}

e 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;
}

Possiamo compilarlo ed eseguirlo con JavaCPP usando questo comando:

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

Con una CPU Intel (R) Core (TM) i7-7700HQ a 2.80 GHz, Fedora 30, GCC 9.1.1 e OpenJDK 8 o 11, ottengo questo tipo di output:

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

O circa 2,4 volte più veloce. Dobbiamo usare buffer NIO diretti invece di array, ma HotSpot può accedere ai buffer NIO diretti alla stessa velocità degli array . D'altra parte, lo srotolamento manuale del loop non fornisce un aumento misurabile delle prestazioni, in questo caso.


3
Hai usato OpenJDK o Oracle HotSpot? Contrariamente alla credenza popolare, non sono la stessa cosa.
Jonathan S. Fisher

@exabrial Questo è ciò che "java -version" restituisce su questa macchina in questo momento: versione java "1.6.0_22" OpenJDK Runtime Environment (IcedTea6 1.10.6) (fedora-63.1.10.6.fc15-x86_64) OpenJDK 64-Bit Server VM (build 20.0-b11, modalità mista)
Samuel Audet

1
Quel loop probabilmente ha una dipendenza del loop trasportato. Puoi aumentare ulteriormente la velocità svolgendo il ciclo due o più volte.

3
@Oliv GCC vettorializza il codice con SSE, sì, ma per dati così piccoli, l'overhead della chiamata JNI è purtroppo troppo grande.
Samuel Audet

2
Sul mio A6-7310 con JDK 13, ottengo: punto (): 69 ns / dotc (): 95 ns. Java vince!
Stefan Reich

39

Per affrontare parte dello scetticismo espresso da altri qui suggerisco a chiunque voglia dimostrare a se stesso o ad altri di utilizzare il seguente metodo:

  • Crea un progetto JMH
  • Scrivi un piccolo frammento di matematica vettorializzabile.
  • Esegui il loro benchmark sfogliando tra -XX: -UseSuperWord e -XX: + UseSuperWord (predefinito)
  • Se non si osserva alcuna differenza nelle prestazioni, probabilmente il codice non è stato vettorializzato
  • Per esserne sicuri, esegui il benchmark in modo che stampi l'assieme. Su Linux puoi goderti il ​​perfasm profiler ('- prof perfasm') dare un'occhiata e vedere se le istruzioni che ti aspetti vengono generate.

Esempio:

@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
}

Il risultato con e senza flag (sul recente laptop Haswell, Oracle JDK 8u60): -XX: + UseSuperWord: 475.073 ± 44.579 ns / op (nanosecondi per op) -XX: -UseSuperWord: 3376.364 ± 233.211 ns / op

L'assembly per l'hot loop è un po 'troppo da formattare e incollare qui, ma ecco uno snippet (hsdis.so non riesce a formattare alcune delle istruzioni vettoriali AVX2, quindi ho eseguito con -XX: UseAVX = 1): -XX: + UseSuperWord (con '-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

Divertiti a prendere d'assalto il castello!


1
Dallo stesso articolo: "l'output del disassemblatore JITed suggerisce che in realtà non è così efficiente in termini di chiamata delle istruzioni SIMD ottimali e della loro pianificazione. Una rapida ricerca attraverso il codice sorgente del compilatore JIT JVM (Hotspot) suggerisce che ciò è dovuto a la non esistenza di codici di istruzione SIMD compressi. " I registri SSE vengono utilizzati in modalità scalare.
Aleksandr Dubinsky

1
@AleksandrDubinsky alcuni casi sono coperti, altri no. Hai un caso concreto che ti interessa?
Nitsan Wakart

2
Capovolgiamo la domanda e chiediamoci se la JVM autovectorize eventuali operazioni aritmetiche. Puoi fornire un esempio? Ho un ciclo che ho dovuto estrarre e riscrivere usando gli intrinseci di recente. Tuttavia, piuttosto che sperare nell'autovettorizzazione, mi piacerebbe vedere il supporto per la vettorizzazione / intrinseca esplicita (simile a agner.org/optimize/vectorclass.pdf ). Ancora meglio sarebbe scrivere un buon backend Java per Aparapi (sebbene la leadership di quel progetto abbia degli obiettivi sbagliati). Lavori sulla JVM?
Aleksandr Dubinsky

1
@AleksandrDubinsky Spero che la risposta estesa aiuti, se non forse un'e-mail lo farebbe. Nota anche che "riscrivi usando gli intrinseci" implica che hai cambiato il codice JVM per aggiungere nuovi elementi intrinseci, è questo che intendi? Immagino che intendessi sostituire il tuo codice Java con chiamate in un'implementazione nativa tramite JNI
Nitsan Wakart

1
Grazie. Questa dovrebbe ora essere la risposta ufficiale. Penso che dovresti rimuovere il riferimento alla carta, poiché è obsoleta e non mostra la vettorizzazione.
Aleksandr Dubinsky

26

Nelle versioni HotSpot che iniziano con Java 7u40, il compilatore del server fornisce il supporto per l'auto-vettorizzazione. Secondo JDK-6340864

Tuttavia, questo sembra essere vero solo per i "loop semplici", almeno per il momento. Ad esempio, l'accumulo di un array non può essere ancora vettorizzato JDK-7192383


La vettorizzazione è presente anche in JDK6 in alcuni casi, sebbene il set di istruzioni SIMD mirato non sia così ampio.
Nitsan Wakart

3
Il supporto alla vettorizzazione del compilatore in HotSpot è stato migliorato molto recentemente (giugno 2017) grazie ai contributi di Intel. Dal punto di vista delle prestazioni, il jdk9 (b163 e versioni successive) ancora inedito vince attualmente su jdk8 a causa di correzioni di bug che abilitano AVX2. I cicli devono soddisfare alcuni vincoli affinché l'auto-vettorizzazione funzioni, ad esempio, utilizzare: int contatore, incremento costante del contatore, una condizione di terminazione con variabili invarianti di ciclo, corpo del ciclo senza chiamate di metodo (?), Nessun ciclo manuale spiegato! I dettagli sono disponibili in: cr.openjdk.java.net/~vlivanov/talks/…
Vedran

Il supporto di aggiunta fusa multipla vettorizzata (FMA) attualmente non sembra buono (a giugno 2017): si tratta di vettorizzazione o FMA scalare (?). Tuttavia, a quanto pare, Oracle ha appena accettato il contributo di Intel all'HotSpot che consente la vettorizzazione FMA utilizzando AVX-512. Per la gioia degli appassionati di auto-vettorizzazione e di quei fortunati ad avere accesso all'hardware AVX-512, questo potrebbe (con un po 'di fortuna) apparire in una delle prossime build EA jdk9 (oltre a b175).
Vedran

Un link per supportare la dichiarazione precedente (RFR (M): 8181616: Vettorizzazione FMA su x86): mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2017-June/…
Vedran

2
Un piccolo benchmark che dimostra l'accelerazione di un fattore 4 su numeri interi attraverso la vettorizzazione del loop utilizzando le istruzioni AVX2: prestodb.rocks/code/simd
Vedran

6

Ecco un bell'articolo sulla sperimentazione con le istruzioni Java e SIMD scritto da un mio amico: http://prestodb.rocks/code/simd/

Il risultato generale è che puoi aspettarti che JIT utilizzi alcune operazioni SSE nella 1.8 (e altre ancora nella 1.9). Anche se non dovresti aspettarti molto e devi stare attento.


1
Sarebbe utile se riassumessi alcune informazioni chiave dell'articolo a cui ti sei collegato.
Aleksandr Dubinsky

4

Potresti scrivere il kernel OpenCl per fare il calcolo ed eseguirlo da java http://www.jocl.org/ .

Il codice può essere eseguito su CPU e / o GPU e il linguaggio OpenCL supporta anche i tipi vettoriali, quindi dovresti essere in grado di trarre vantaggio esplicito, ad esempio, dalle istruzioni SSE3 / 4.


4

Dai un'occhiata al Confronto delle prestazioni tra Java e JNI per l'implementazione ottimale di micro-kernel computazionali . Mostrano che il compilatore del server Java HotSpot VM supporta la vettorizzazione automatica utilizzando il parallelismo a livello di super parole, che è limitato a casi semplici di parallelismo all'interno del ciclo. Questo articolo ti fornirà anche alcune indicazioni se le dimensioni dei tuoi dati sono abbastanza grandi da giustificare la rotta JNI.


3

Immagino che tu abbia scritto questa domanda prima di scoprire netlib-java ;-) fornisce esattamente l'API nativa di cui hai bisogno, con implementazioni ottimizzate per la macchina, e non ha alcun costo al limite nativo grazie al pinning della memoria.


1
Sì, molto tempo fa. Speravo di più di sentire che questo è tradotto automaticamente in istruzioni vettorializzate. Ma chiaramente non è così difficile farlo accadere manualmente.
Sean Owen

-4

Non credo che la maggior parte delle VM siano mai abbastanza intelligenti per questo tipo di ottimizzazioni. Per essere onesti, la maggior parte delle ottimizzazioni sono molto più semplici, come lo spostamento invece della moltiplicazione quando si ha un potere di due. Il progetto mono ha introdotto il proprio vettore e altri metodi con supporti nativi per aiutare le prestazioni.


3
Attualmente, nessun compilatore di hotspot Java lo fa, ma non è molto più difficile delle cose che fanno. Usano le istruzioni SIMD per copiare più valori di array contemporaneamente. Devi solo scrivere un po 'più di corrispondenza dei modelli e codice di generazione del codice, il che è piuttosto semplice dopo aver fatto un po' di srotolamento del ciclo. Penso che le persone alla Sun siano diventate pigre, ma sembra che ora accadrà a Oracle (yay Vladimir! Questo dovrebbe aiutare molto il nostro codice!): Mail.openjdk.java.net/pipermail/hotspot-compiler-dev/ ...
Christopher Manning,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.