Disons que le goulot d'étranglement de mon programme Java est vraiment des boucles serrées pour calculer un tas de produits vectoriels à points. Oui, j'ai profilé, oui c'est le goulot d'étranglement, oui c'est important, oui c'est juste comme l'algorithme est, oui j'ai exécuté Proguard pour optimiser le code d'octet, etc.
Le travail est essentiellement des produits scalaires. Comme dans, j'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 type 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 ne compilera pas. Est-ce que quelqu'un jamais a entendu parler d'un code de génération JIT qui utilise 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"; mérite d'être demandé.
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 ne fonctionnait pas pour vous, mais permettez-moi de 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 suivantes:
$ 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 type de sortie:
dot(): 37 ns
dotc(): 23 ns
Ou environ 1,6 fois plus rapide. Nous devons utiliser des tampons NIO directs au lieu des tableaux, mais HotSpot peut accéder aux tampons NIO directs aussi rapidement que les tableaux . D'un autre côté, le déroulement manuel de la boucle ne fournit pas une augmentation mesurable des performances, dans ce cas.
Pour répondre à certains des scepticismes exprimés par d'autres ici, je suggère à tous ceux qui veulent se prouver à eux-mêmes ou à d'autres d'utiliser la méthode suivante:
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 indicateur (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 de la boucle chaude est un peu trop à 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!
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 ê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-719238
Voici un bel article sur l'expérimentation de Java et les instructions 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 deviez pas en attendre beaucoup et que vous deviez être prudent.
Vous pouvez écrire le noyau OpenCl pour faire l'informatique 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 d'exploiter explicitement, par exemple Instructions SSE3/4.
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 frontière native grâce à l'épinglage de la mémoire.
Jetez un œil à Comparaison des performances entre Java et JNI pour une implémentation optimale des micro-noyaux de calcul . Ils montrent que Java HotSpot = VM prend en charge la vectorisation automatique à l'aide du parallélisme de niveau Super-Word, qui est limité aux cas simples 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 la route JNI.