Les compilateurs JIT de JVM génèrent-ils du code qui utilise des instructions en virgule flottante vectorisées?


95

Disons que le goulot d'étranglement de mon programme Java est vraiment quelques boucles serrées pour calculer un tas de produits scalaires vectoriels. Oui j'ai profilé, oui c'est le goulot d'étranglement, oui c'est significatif, oui c'est juste comme ça que l'algorithme est, oui j'ai exécuté Proguard pour optimiser le byte code, etc.

Le travail consiste essentiellement en des produits scalaires. Comme dans, j'en ai deux float[50]et je dois calculer la somme des produits par paire. Je sais que des jeux d'instructions de processeur existent pour effectuer ce genre d'opérations rapidement et en masse, comme SSE ou MMX.

Oui, je peux probablement y accéder en écrivant du code natif dans JNI. L'appel JNI s'avère assez cher.

Je sais que vous ne pouvez pas garantir ce qu'un JIT compilera ou non. Quelqu'un a-t-il déjà entendu parler d'un code de génération JIT utilisant ces instructions? et si oui, y a-t-il quelque chose dans le code Java qui aide à le rendre compilable de cette façon?

Probablement un "non"; vaut la peine de demander.


4
Le moyen le plus simple de le savoir est probablement d'obtenir le JIT le plus moderne que vous puissiez trouver et de lui faire afficher l'assembly généré -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:+LogCompilation. Vous aurez besoin d'un programme qui exécute la méthode vectorisable suffisamment de fois pour la rendre "chaude".
Louis Wasserman

1
Ou jetez un œil à la source. download.java.net/openjdk/jdk7
Projet de loi

1
"Prochainement" dans un jdk près de chez vous: mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2012-July
Jonathan S. Fisher

3
En fait, selon ce blog , JNI peut être assez rapide s'il est utilisé "correctement".
ziggystar

2
Un article de blog pertinent à ce sujet peut être trouvé ici: psy-lob-saw.blogspot.com/2015/04/… avec le message général que la vectorisation peut se produire et se produit. Outre la vectorisation de cas spécifiques (Arrays.fill () / equals (char []) / arrayCopy), la JVM vectorise automatiquement à l'aide de la parallélisation au niveau des super mots. Le code pertinent est dans superword.cpp et l'article sur lequel il est basé est ici: groups.csail.mit.edu/cag/slp/SLP-PLDI-2000.pdf
Nitsan Wakart

Réponses:


44

Donc, fondamentalement, vous voulez que votre code s'exécute plus rapidement. JNI est la réponse. Je sais que vous avez dit que cela n'a pas fonctionné pour vous, mais laissez-moi vous montrer que vous vous trompez.

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

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

Nous pouvons compiler et exécuter cela avec JavaCPP en utilisant cette commande:

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

Avec un processeur Intel (R) Core (TM) i7-7700HQ à 2,80 GHz, Fedora 30, GCC 9.1.1 et OpenJDK 8 ou 11, j'obtiens ce type de sortie:

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

Ou environ 2,4 fois plus rapide. Nous devons utiliser des tampons NIO directs au lieu de baies, mais HotSpot peut accéder aux tampons NIO directs aussi rapidement que des baies . En revanche, le déroulement manuel de la boucle n'apporte pas une amélioration mesurable des performances, dans ce cas.


3
Avez-vous utilisé OpenJDK ou Oracle HotSpot? Contrairement à la croyance populaire, ils ne sont pas les mêmes.
Jonathan S.Fisher

@exabrial Voici ce que "java -version" renvoie sur cette machine en ce moment: version java "1.6.0_22" Environnement d'exécution OpenJDK (IcedTea6 1.10.6) (fedora-63.1.10.6.fc15-x86_64) VM serveur OpenJDK 64 bits (build 20.0-b11, mode mixte)
Samuel Audet

1
Cette boucle a probablement une dépendance de boucle portée. Vous pouvez obtenir une accélération supplémentaire en déroulant la boucle deux ou plusieurs fois.

3
@Oliv GCC vectorise le code avec SSE, oui, mais pour des données aussi petites, le surcoût d'appel JNI est malheureusement trop important.
Samuel Audet

2
Sur mon A6-7310 avec JDK 13, j'obtiens: dot (): 69 ns / dotc (): 95 ns. Java gagne!
Stefan Reich le

39

Pour répondre à une partie du scepticisme exprimé par d'autres ici, je suggère à quiconque souhaite se prouver à lui-même ou à d'autres d'utiliser la méthode suivante:

  • Créer un projet JMH
  • Écrivez un petit extrait de mathématiques vectorisables.
  • Exécutez leur benchmark en basculant entre -XX: -UseSuperWord et -XX: + UseSuperWord (par défaut)
  • Si aucune différence de performances n'est observée, votre code n'a probablement pas été vectorisé
  • Pour vous en assurer, exécutez votre benchmark de sorte qu'il imprime l'assemblage. Sous Linux, vous pouvez profiter du profileur de perfasm ('- prof perfasm'), jeter un œil et voir si les instructions que vous attendez sont générées.

Exemple:

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

Le résultat avec et sans le drapeau (sur un ordinateur portable récent Haswell, Oracle JDK 8u60): -XX: + UseSuperWord: 475,073 ± 44,579 ns / op (nanosecondes par opération) -XX: -UseSuperWord: 3376,364 ± 233,211 ns / op

L'assemblage pour la boucle chaude est un peu difficile à formater et à coller ici, mais voici un extrait (hsdis.so ne parvient pas à formater certaines des instructions vectorielles AVX2, j'ai donc exécuté avec -XX: UseAVX = 1): -XX: + UseSuperWord (avec '-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

Amusez-vous à prendre d'assaut le château!


1
D'après le même article: «La sortie du désassembleur JITed suggère qu'il n'est pas vraiment efficace en termes d'appels des instructions SIMD les plus optimales et de leur planification. Une recherche rapide dans le code source du compilateur JVM JIT (Hotspot) suggère que cela est dû à l'inexistence de codes d'instructions SIMD compressés. " Les registres SSE sont utilisés en mode scalaire.
Aleksandr Dubinsky

1
@AleksandrDubinsky, certains cas sont couverts, d'autres non. Vous avez un cas concret qui vous intéresse?
Nitsan Wakart

2
Retournons la question et demandons si la JVM va autovectoriser des opérations arithmétiques? Pouvez vous donner un exemple? J'ai une boucle que j'ai dû extraire et réécrire en utilisant les intrinsèques récemment. Cependant, plutôt que d'espérer une autovectorisation, j'aimerais voir un support pour la vectorisation explicite / intrinsèques (similaire à agner.org/optimize/vectorclass.pdf ). Encore mieux serait d'écrire un bon backend Java pour Aparapi (bien que la direction de ce projet ait des objectifs erronés). Travaillez-vous sur la JVM?
Aleksandr Dubinsky

1
@AleksandrDubinsky J'espère que la réponse étendue aidera, sinon peut-être un e-mail. Notez également que «réécrire en utilisant des intrinsèques» implique que vous avez changé le code JVM pour ajouter de nouveaux intrinsèques, est-ce que vous voulez dire? Je suppose que vous vouliez remplacer votre code Java par des appels dans une implémentation native via JNI
Nitsan Wakart

1
Je vous remercie. Cela devrait maintenant être la réponse officielle. Je pense que vous devriez supprimer la référence au papier, car il est obsolète et ne démontre pas de vectorisation.
Aleksandr Dubinsky du

26

Dans les versions HotSpot commençant par Java 7u40, le compilateur de serveur prend en charge la vectorisation automatique. Selon JDK-6340864

Cependant, cela ne semble vrai que pour les «boucles simples» - du moins pour le moment. Par exemple, l'accumulation d'un tableau ne peut pas encore être vectorisée JDK-7192383


La vectorisation est également présente dans JDK6 dans certains cas, bien que le jeu d'instructions SIMD ciblé ne soit pas aussi large.
Nitsan Wakart

3
La prise en charge de la vectorisation du compilateur dans HotSpot a été beaucoup améliorée ces derniers temps (juin 2017) grâce aux contributions d'Intel. En termes de performances, le jdk9 (b163 et ultérieur), encore inédit, l'emporte sur jdk8 en raison de corrections de bogues activant AVX2. Les boucles doivent remplir quelques contraintes pour que l'auto-vectorisation fonctionne, par exemple utiliser: compteur int, incrément de compteur constant, une condition de terminaison avec des variables invariantes de boucle, corps de boucle sans appels de méthode (?), Pas de déploiement manuel de boucle! Les détails sont disponibles sur: cr.openjdk.java.net/~vlivanov/talks/…
Vedran

Le support vectorisé fused-multiple-add (FMA) ne semble pas bon actuellement (à partir de juin 2017): il s'agit soit de vectorisation, soit de FMA scalaire (?). Cependant, Oracle vient apparemment d'accepter la contribution d'Intel au HotSpot qui permet la vectorisation FMA à l'aide de l'AVX-512. Pour le plus grand plaisir des fans de vectorisation automatique et des chanceux d'avoir accès au matériel AVX-512, cela peut (avec un peu de chance) apparaître dans l'une des prochaines versions de jdk9 EA (au-delà de b175).
Vedran

Un lien pour soutenir la déclaration précédente (RFR (M): 8181616: Vectorisation FMA sur x86): mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2017-June
Vedran

2
Un petit benchmark démontrant une accélération d'un facteur 4 sur des entiers grâce à la vectorisation de boucle à l'aide d'instructions AVX2: prestodb.rocks/code/simd
Vedran

6

Voici un bel article sur l'expérimentation des instructions Java et SIMD écrites par mon ami: http://prestodb.rocks/code/simd/

Son résultat général est que vous pouvez vous attendre à ce que JIT utilise certaines opérations SSE en 1.8 (et d'autres en 1.9). Bien que vous ne devriez pas vous attendre à grand-chose et que vous devez être prudent.


1
Cela vous aiderait si vous résumiez quelques idées clés de l'article auquel vous avez lié.
Aleksandr Dubinsky

4

Vous pouvez écrire le noyau OpenCl pour faire le calcul et l'exécuter à partir de java http://www.jocl.org/ .

Le code peut être exécuté sur le CPU et / ou le GPU et le langage OpenCL prend également en charge les types vectoriels, vous devriez donc être en mesure de tirer explicitement parti des instructions SSE3 / 4, par exemple.


4

Jetez un œil à la comparaison des performances entre Java et JNI pour une implémentation optimale des micro-noyaux de calcul . Ils montrent que le compilateur de serveur Java HotSpot VM prend en charge la vectorisation automatique à l'aide du parallélisme de niveau super-mot, qui est limité à de simples cas de parallélisme à l'intérieur de la boucle. Cet article vous indiquera également si la taille de vos données est suffisamment grande pour justifier une route JNI.


3

Je suppose que vous avez écrit cette question avant de découvrir netlib-java ;-) il fournit exactement l'API native dont vous avez besoin, avec des implémentations optimisées pour la machine, et n'a aucun coût à la limite native grâce à l'épinglage de la mémoire.


1
Ouais, il y a longtemps. J'espérais plus entendre que cela se traduit automatiquement en instructions vectorisées. Mais ce n'est clairement pas si difficile de le faire manuellement.
Sean Owen

-4

Je ne pense pas que la plupart des machines virtuelles soient suffisamment intelligentes pour ce type d'optimisation. Pour être juste, la plupart des optimisations sont beaucoup plus simples, comme le décalage au lieu de la multiplication quand une puissance de deux. Le projet mono a introduit son propre vecteur et d'autres méthodes avec des supports natifs pour améliorer les performances.


3
Actuellement, aucun compilateur de hotspot Java ne le fait, mais ce n'est pas beaucoup plus difficile que ce qu'ils font. Ils utilisent les instructions SIMD pour copier plusieurs valeurs de tableau à la fois. Il vous suffit d'écrire un peu plus de correspondance de modèle et de code de génération de code, ce qui est assez simple après avoir déroulé une boucle. Je pense que les gens de Sun ont juste été paresseux, mais il semble que cela se produira maintenant chez Oracle (ouais Vladimir! Cela devrait beaucoup aider notre code!): Mail.openjdk.java.net/pipermail/hotspot-compiler-dev/ …
Christopher Manning
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.