Est-ce que les compilateurs JIT de JVM génèrent du code qui utilise des instructions à virgule flottante vectorisées?

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

Le travail est, essentiellement, des produits dot. Comme dans, j'ai deux float[50] et j'ai besoin de calculer la somme des produits par paires. Je sais que des ensembles d'instructions de processeur existent pour effectuer ce genre d'opérations rapidement et dans en vrac, comme SSE ou MMX.

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

Je sais que vous ne pouvez pas garantir ce QU'un JIT va compiler ou ne pas compiler. Est-ce que quelqu'un a déjà entendu parler d'un code générateur de JIT qui utilise ces instructions? et si oui, y a-t-il quelque chose à propos du code Java qui aide à le rendre compilable de cette façon?

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

87
demandé sur Sean Owen 2012-05-28 16:48:21

8 réponses

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

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 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 les commandes line:

$ javac -cp javacpp.jar Dot.java
$ java -jar javacpp.jar Dot
$ java -cp javacpp.jar:. Dot

Avec un processeur Intel Core i7-3632QM @ 2.20 GHz, Fedora 20, GCC 4.8.3 et OpenJDK 7 ou 8, j'obtiens ce genre de sortie:

dot(): 37 ns
dotc(): 23 ns

Soit environ 1,6 fois plus vite. Nous avons besoin de utilisez des tampons NIO directs au lieu de tableaux, mais HotSpot peut accéder aux tampons NIO directs aussi vite que les tableaux. D'autre part, le déroulement manuel de la boucle ne fournit pas un coup de pouce mesurable dans les performances, dans ce cas.

39
répondu Samuel Audet 2014-12-06 02:26:19

Pour répondre à certains du scepticisme exprimé par d'autres ici, je suggère à quiconque veut prouver à eux-mêmes ou d'autres utiliser la méthode suivante:

  • Créer un projet JMH
  • écrivez un petit extrait de mathématiques vectorisables.
  • exécutez leur retournement de référence entre-XX: - UseSuperWord et-XX: + UseSuperWord (par défaut)
  • si aucune différence de performance n'est observée, votre code n'a probablement pas été vectorisé
  • pour vous assurer, exécutez votre benchmark de sorte qu'il imprime l'assemblée. Sous linux, vous pouvez profiter du profileur perfasm ('- prof perfasm') pour 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 Haswell récent, Oracle JDK 8u60): - XX: + UseSuperWord: 475.073 ± 44.579 ns / op (nanosecondes par op) - XX: - UseSuperWord: 3376.364 ± 233.211 ns / op

L'assemblage pour la boucle chaude est un peu beaucoup à formater et à coller ici Mais voici un extrait(hsdis.so est a défaut de formater certaines des instructions vectorielles AVX2, j'ai donc couru 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!

35
répondu Nitsan Wakart 2017-10-09 21:12:16

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

Cependant, cela ne semble être 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

25
répondu Vedran 2013-11-28 13:30:13

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

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

5
répondu kokosing 2017-06-28 20:09:59

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 CPU et / ou GPU et le langage OpenCL prend également en charge les types de vecteurs, vous devriez donc pouvoir profiter explicitement des instructions SSE3/4 par exemple.

4
répondu Mikael Lepistö 2013-02-28 15:02:34

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 par machine, et n'a aucun coût à la limite native due à l'épinglage de la mémoire.

3
répondu fommil 2015-12-01 16:57:21

Jetez un oeil à Comparaison des performances entre Java et JNI pour une implémentation optimale des micro-noyaux de calcul . Ils montrent que le compilateur Java HotSpot VM Server prend en charge la vectorisation automatique en utilisant le parallélisme au niveau des Super-mots, qui est limité aux cas simples de parallélisme à l'intérieur de la boucle. Cet article vous donnera également quelques conseils pour savoir si la taille de vos données est suffisamment grande pour justifier l'itinéraire JNI.

3
répondu Paul Jurczak 2015-12-05 23:11:34

Je ne crois pas que la plupart des machines virtuelles soient assez intelligentes pour ce genre d'optimisations. Pour être juste, la plupart des optimisations sont beaucoup plus simples, comme le déplacement au lieu de la multiplication whena puissance de deux. Le projet mono a introduit son propre vecteur et d'autres méthodes avec des supports natifs pour aider les performances.

-3
répondu mP. 2012-05-30 02:40:42