Generieren die JIT-Compiler einer JVM Code, der vektorisierte Gleitkommaanweisungen verwendet?

95

Angenommen, der Engpass meines Java-Programms besteht in einigen engen Schleifen, um eine Reihe von Vektorpunktprodukten zu berechnen. Ja, ich habe ein Profil erstellt, ja, es ist der Engpass, ja, es ist signifikant, ja, so ist der Algorithmus, ja, ich habe Proguard ausgeführt, um den Bytecode zu optimieren usw.

Die Arbeit besteht im Wesentlichen aus Punktprodukten. Wie in habe ich zwei float[50]und ich muss die Summe der paarweisen Produkte berechnen. Ich weiß, dass Prozessorbefehlssätze existieren, um diese Art von Operationen schnell und in großen Mengen auszuführen, wie z. B. SSE oder MMX.

Ja, ich kann wahrscheinlich darauf zugreifen, indem ich nativen Code in JNI schreibe. Der JNI-Anruf erweist sich als ziemlich teuer.

Ich weiß, dass Sie nicht garantieren können, was eine JIT kompiliert oder nicht kompiliert. Hat jemand jemals von einem JIT-Generierungscode gehört, der diese Anweisungen verwendet? Und wenn ja, gibt es irgendetwas an dem Java-Code, das dazu beiträgt, ihn auf diese Weise kompilierbar zu machen?

Wahrscheinlich ein "Nein"; es lohnt sich zu fragen.

Sean Owen
quelle
4
Der einfachste Weg, dies herauszufinden, besteht wahrscheinlich darin, die modernste JIT zu erhalten, mit der Sie die generierte Baugruppe ausgeben können -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:+LogCompilation. Sie benötigen ein Programm, das die vektorisierbare Methode so oft ausführt, dass sie "heiß" wird.
Louis Wasserman
1
Oder schauen Sie sich die Quelle an. download.java.net/openjdk/jdk7
Bill
1
"In Kürze" zu einem JDK in Ihrer Nähe: mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2012-July/…
Jonathan S. Fisher
3
Laut diesem Blog kann JNI tatsächlich ziemlich schnell sein, wenn es "richtig" verwendet wird.
Ziggystar
2
Einen relevanten Blog-Beitrag dazu finden Sie hier: psy-lob-saw.blogspot.com/2015/04/… mit der allgemeinen Nachricht, dass Vektorisierung passieren kann und passiert. Abgesehen von der Vektorisierung bestimmter Fälle (Arrays.fill () / equals (char []) / arrayCopy) wird die JVM mithilfe der Superword Level Parallelization automatisch vektorisiert. Der relevante Code befindet sich in superword.cpp und das Papier, auf dem er basiert, ist hier: groups.csail.mit.edu/cag/slp/SLP-PLDI-2000.pdf
Nitsan Wakart

Antworten:

44

Grundsätzlich möchten Sie, dass Ihr Code schneller ausgeführt wird. JNI ist die Antwort. Ich weiß, dass du gesagt hast, dass es bei dir nicht funktioniert hat, aber ich möchte dir zeigen, dass du falsch liegst.

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

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

Mit diesem Befehl können wir das mit JavaCPP kompilieren und ausführen :

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

Mit einer Intel (R) Core (TM) i7-7700HQ-CPU bei 2,80 GHz, Fedora 30, GCC 9.1.1 und OpenJDK 8 oder 11 erhalte ich diese Art von Ausgabe:

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

Oder ungefähr 2,4 mal schneller. Wir müssen direkte NIO-Puffer anstelle von Arrays verwenden, aber HotSpot kann genauso schnell wie Arrays auf direkte NIO-Puffer zugreifen . Andererseits führt das manuelle Abrollen der Schleife in diesem Fall nicht zu einer messbaren Leistungssteigerung.

Samuel Audet
quelle
3
Haben Sie OpenJDK oder Oracle HotSpot verwendet? Entgegen der landläufigen Meinung sind sie nicht gleich.
Jonathan S. Fisher
@exabrial Dies ist, was "Java-Version" gerade auf diesem Computer zurückgibt: Java-Version "1.6.0_22" OpenJDK-Laufzeitumgebung (IcedTea6 1.10.6) (fedora-63.1.10.6.fc15-x86_64) OpenJDK 64-Bit-Server-VM (Build 20.0-b11, gemischter Modus)
Samuel Audet
1
Diese Schleife hat wahrscheinlich eine übertragene Schleifenabhängigkeit. Sie können eine weitere Beschleunigung erzielen, indem Sie die Schleife zwei- oder mehrmals abrollen.
3
@Oliv GCC vektorisiert den Code mit SSE, ja, aber für so kleine Daten ist der JNI-Anrufaufwand leider zu groß.
Samuel Audet
2
Auf meinem A6-7310 mit JDK 13 erhalte ich: dot (): 69 ns / dotc (): 95 ns. Java gewinnt!
Stefan Reich
39

Um einige der hier von anderen geäußerten Skepsis anzusprechen, schlage ich jedem vor, der sich selbst oder anderen beweisen möchte, die folgende Methode anzuwenden:

  • Erstellen Sie ein JMH-Projekt
  • Schreiben Sie einen kleinen Ausschnitt aus vektorisierbarer Mathematik.
  • Führen Sie den Benchmark-Wechsel zwischen -XX: -UseSuperWord und -XX: + UseSuperWord aus (Standard).
  • Wenn kein Leistungsunterschied festgestellt wird, wurde Ihr Code wahrscheinlich nicht vektorisiert
  • Führen Sie Ihren Benchmark so aus, dass die Baugruppe ausgedruckt wird. Unter Linux können Sie den Perfasm-Profiler ('- prof perfasm') genießen. Schauen Sie nach, ob die erwarteten Anweisungen generiert werden.

Beispiel:

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

Das Ergebnis mit und ohne Flag (auf dem aktuellen Haswell-Laptop Oracle JDK 8u60): -XX: + UseSuperWord: 475,073 ± 44,579 ns / op (Nanosekunden pro op) -XX: -UseSuperWord: 3376,364 ± 233,211 ns / op

Die Assembly für die Hot-Loop ist ein bisschen viel zu formatieren und hier zu bleiben, aber hier ist ein Ausschnitt (hsdis.so kann einige der AVX2-Vektoranweisungen nicht formatieren, daher habe ich -XX: UseAVX = 1 ausgeführt): -XX: + UseSuperWord (mit '-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

Viel Spaß beim Stürmen der Burg!

Nitsan Wakart
quelle
1
Aus demselben Artikel: "Die Ausgabe des JITed-Disassemblers deutet darauf hin, dass es nicht so effizient ist, die optimalsten SIMD-Anweisungen und deren Planung aufzurufen. Eine schnelle Suche durch den JVM-JIT-Compiler (Hotspot) -Quellcode legt nahe, dass dies darauf zurückzuführen ist das Nichtvorhandensein gepackter SIMD-Anweisungscodes. " Die SSE-Register werden im Skalarmodus verwendet.
Aleksandr Dubinsky
1
@AleksandrDubinsky Einige Fälle werden behandelt, andere nicht. Haben Sie einen konkreten Fall, an dem Sie interessiert sind?
Nitsan Wakart
2
Lassen Sie uns die Frage umdrehen und fragen, ob die JVM arithmetische Operationen automatisch automatisiert. Können Sie ein Beispiel geben? Ich habe eine Schleife, die ich kürzlich mithilfe von Intrinsics herausziehen und neu schreiben musste. Anstatt jedoch auf eine Autovektorisierung zu hoffen, würde ich gerne Unterstützung für eine explizite Vektorisierung / Intrinsics sehen (ähnlich wie bei agner.org/optimize/vectorclass.pdf ). Noch besser wäre es, ein gutes Java-Backend für Aparapi zu schreiben (obwohl die Leitung dieses Projekts einige falsche Ziele hat). Arbeiten Sie an der JVM?
Aleksandr Dubinsky
1
@AleksandrDubinsky Ich hoffe, die erweiterte Antwort hilft, wenn nicht vielleicht eine E-Mail. Beachten Sie auch, dass "Umschreiben mit Intrinsics" impliziert, dass Sie den JVM-Code geändert haben, um neue Intrinsics hinzuzufügen. Ist das das, was Sie meinen? Ich
vermute, Sie wollten
1
Danke dir. Dies sollte nun die offizielle Antwort sein. Ich denke, Sie sollten den Verweis auf das Papier entfernen, da es veraltet ist und keine Vektorisierung zeigt.
Aleksandr Dubinsky
26

In HotSpot-Versionen, die mit Java 7u40 beginnen, bietet der Server-Compiler Unterstützung für die automatische Vektorisierung. Laut JDK-6340864

Dies scheint jedoch nur für "einfache Schleifen" zu gelten - zumindest für den Moment. Beispielsweise kann das Akkumulieren eines Arrays noch nicht vektorisiert werden. JDK-7192383

Vedran
quelle
In einigen Fällen ist auch in JDK6 eine Vektorisierung vorhanden, obwohl der angestrebte SIMD-Befehlssatz nicht so breit ist.
Nitsan Wakart
3
Die Unterstützung der Compiler-Vektorisierung in HotSpot wurde in letzter Zeit (Juni 2017) aufgrund von Beiträgen von Intel erheblich verbessert. In Bezug auf die Leistung gewinnt das noch unveröffentlichte jdk9 (b163 und höher) derzeit aufgrund von Fehlerkorrekturen, die AVX2 ermöglichen, gegen jdk8. Schleifen müssen einige Einschränkungen erfüllen, damit die automatische Vektorisierung funktioniert, z. B. Verwendung: int-Zähler, konstantes Zählerinkrement, eine Beendigungsbedingung mit schleifeninvarianten Variablen, Schleifenkörper ohne Methodenaufrufe (?), Keine manuelle Schleifenentfaltung! Details sind verfügbar in: cr.openjdk.java.net/~vlivanov/talks/…
Vedran
Die Unterstützung für vektorisiertes Fused-Multiple-Add (FMA) sieht derzeit (Stand Juni 2017) nicht gut aus: Es handelt sich entweder um Vektorisierung oder um skalare FMA (?). Oracle hat jedoch offenbar gerade den Beitrag von Intel zum HotSpot akzeptiert, der die FMA-Vektorisierung mit AVX-512 ermöglicht. Zur Freude der Fans der automatischen Vektorisierung und derjenigen, die Glück haben, Zugriff auf AVX-512-Hardware zu haben, kann dies (mit etwas Glück) in einem der nächsten jdk9 EA-Builds (über b175 hinaus) erscheinen.
Vedran
Ein Link zur Unterstützung der vorherigen Anweisung (RFR (M): 8181616: FMA-Vektorisierung auf x86): mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2017-June/…
Vedran
2
Ein kleiner Benchmark, der die Beschleunigung von ganzen Zahlen durch Schleifenvektorisierung um den Faktor 4 unter Verwendung von AVX2-Anweisungen demonstriert
Vedran
6

Hier ist ein schöner Artikel über das Experimentieren mit Java- und SIMD-Anweisungen, den mein Freund geschrieben hat: http://prestodb.rocks/code/simd/

Das allgemeine Ergebnis ist, dass Sie davon ausgehen können, dass JIT in 1.8 einige SSE-Operationen verwendet (und in 1.9 weitere). Sie sollten jedoch nicht viel erwarten und müssen vorsichtig sein.

kokosing
quelle
1
Es wäre hilfreich, wenn Sie einige wichtige Erkenntnisse des Artikels zusammenfassen würden, auf den Sie verlinkt haben.
Aleksandr Dubinsky
4

Sie können den OpenCl-Kernel schreiben, um die Datenverarbeitung durchzuführen, und ihn unter Java http://www.jocl.org/ ausführen .

Code kann auf CPU und / oder GPU ausgeführt werden, und die OpenCL-Sprache unterstützt auch Vektortypen, sodass Sie z. B. SSE3 / 4-Anweisungen explizit nutzen können sollten.

Mikael Lepistö
quelle
4

Schauen Sie sich den Leistungsvergleich zwischen Java und JNI an, um eine optimale Implementierung von rechnergestützten Mikrokernen zu erhalten . Sie zeigen, dass der Java HotSpot VM-Server-Compiler die automatische Vektorisierung mithilfe der Super-Word-Level-Parallelität unterstützt, die auf einfache Fälle von Parallelität innerhalb der Schleife beschränkt ist. In diesem Artikel erfahren Sie auch, ob Ihre Datengröße groß genug ist, um eine JNI-Route zu rechtfertigen.

Paul Jurczak
quelle
3

Ich vermute, Sie haben diese Frage geschrieben, bevor Sie von netlib-java erfahren haben.

Fommil
quelle
1
Ja, vor langer Zeit. Ich hatte mehr gehofft zu hören, dass dies automatisch in vektorisierte Anweisungen übersetzt wird. Aber es ist natürlich nicht so schwer, dies manuell zu erreichen.
Sean Owen
-4

Ich glaube nicht, dass die meisten VMs jemals intelligent genug für diese Art von Optimierungen sind. Um fair zu sein, sind die meisten Optimierungen viel einfacher, z. B. Verschieben statt Multiplizieren bei einer Zweierpotenz. Das Mono-Projekt führte einen eigenen Vektor und andere Methoden mit nativen Backings ein, um die Leistung zu verbessern.

mP.
quelle
3
Derzeit macht dies kein Java-Hotspot-Compiler, aber es ist nicht viel schwieriger als die Dinge, die sie tun. Sie verwenden SIMD-Anweisungen, um mehrere Array-Werte gleichzeitig zu kopieren. Sie müssen nur noch mehr Mustervergleichs- und Codegenerierungscode schreiben, was nach dem Abrollen der Schleife ziemlich einfach ist. Ich denke, die Leute bei Sun sind gerade faul geworden, aber es sieht so aus, als würde es jetzt bei Oracle passieren (yay Vladimir! Dies sollte unserem Code sehr helfen!): Mail.openjdk.java.net/pipermail/hotspot-compiler-dev/ …
Christopher Manning