Comment obtenir l'optimisation SIMD pour HotSpot JavaVM

Conversion de vectorisation HotSpot JavaVM

HotSpot JavaVM récent est optimisé pour vectoriser le traitement itératif des opérations scalaires et le convertir en instructions SIMD (qu'est-ce que SIMD? Voir la seconde moitié). Lorsque je l'ai essayé avec le code qui fonctionne réellement pour l'optimisation, une amélioration de la vitesse de 1,5 à 2,8 fois ** a été constatée, ce qui permet de bien l'utiliser avec les bibliothèques Java qui effectuent beaucoup de traitement arithmétique (ne comptez pas sur GPGPU). Cela peut être une méthode d'optimisation efficace.

L'optimisation SIMD de HotSpot est basée sur Superword-Level Parallelism (SLP) (ci-après, cette optimisation est Appelé SLP). À l'origine, cet article était destiné à refléter les temps et à convertir le traitement d'image / audio non compatible SIMD en instructions qui utilisent SIMD au niveau du compilateur et des couches d'exécution, mais il s'agit d'un code octet Java sans instructions SIMD. Il est également efficace pour utiliser SIMD.

Conversion de vectorisation HotSpot JavaVM

Processus de conversion SLP

Supposons le traitement de boucle simple suivant.

for(int i=0; i<1024; i++){
  a[i] = b[i] + c[i];
}

[Déroulement de la boucle](https://ja.wikipedia.org/wiki/%E3%83%AB%E3%83%BC%E3%83%97%E5%B1%95%E9%96] % 8B) (extension de boucle) est effectuée.

for(int i=0; i<1024; i+=4){
  a[i] = b[i] + c[i];
  a[i+1] = b[i+1] + c[i+1];
  a[i+2] = b[i+2] + c[i+2];
  a[i+3] = b[i+3] + c[i+3];
}

Cela à lui seul réduit le nombre d'interactions, le nombre de jugements de condition et la pénalité pour la prédiction de branche, donc on peut s'attendre à ce qu'il s'accélère (il existe de nombreuses extensions de boucle de ce type qui peuvent être effectuées par un compilateur ou une machine virtuelle).

Maintenant, le traitement à l'intérieur de la boucle peut être considéré comme l'opération vectorielle suivante.

\begin{pmatrix} a_{i+0} \\ a_{i+1} \\ a_{i+2} \\ a_{i+3} \end{pmatrix} = \begin{pmatrix} b_{i+0} \\ b_{i+1} \\ b_{i+2} \\ b_{i+3} \end{pmatrix} + \begin{pmatrix} c_{i+0} \\ c_{i+1} \\ c_{i+2} \\ c_{i+3} \end{pmatrix}

Cette opération vectorielle est en fait remplacée par une instruction d'opération vectorielle.

for(int i=0; i<1000; i+=4){
  load b[i:i+3] to xmm0
  load c[i:i+3] to xmm1
  add xmm0, xmm1 and store result to xmm0
  load xmm0 to a[i:i+3]
}

Avec cette conversion, il est possible de convertir la répétition d'un traitement arithmétique simple en une arithmétique vectorielle. Étant donné que tous les processeurs modernes peuvent utiliser des instructions SIMD, il est efficace d'utiliser SIMD sans changer la source ou le code d'octet.

Comportement en Java 8

Activez / désactivez en fait SLP en utilisant les sources suivantes et comparez les temps d'exécution. Les mesures sont appropriées, donc si vous avez besoin de preuves précises, faites-le vous-même.

import java.util.Arrays;

public class J8SIMD {
  private static final int SIZE = 1024 * 1024;
  private static final float[] a = new float[SIZE];
  private static final float[] b = new float[SIZE];
  static {
    Arrays.fill(a, 1);
    Arrays.fill(b, 2);
  }
  public static void vectorAdd(){
    for(int i=0; i<a.length; i++){
      a[i] += b[i];
    }
  }
  public static void main(String[] args){
    // warming up
    for(int i=0; i<100; i++) vectorAdd();
    // measure
    long t0 = System.currentTimeMillis();
    for(int i=0; i<10000; i++){
      vectorAdd();
    }
    long t1 = System.currentTimeMillis();
    System.out.printf("vectorAdd: %,d[msec]", t1 - t0);
  }
}

L'exécution de la source ci-dessus sur un Core i7-6600U (compatible MMX, SSE, SSE2, SSSE3, SSE4, AVX) fait une différence significative (même si j'ai simplement déroulé une boucle car je ne suis pas sûr d'avoir utilisé l'instruction SIMD). Il n'y a pas de possibilité de juste une différence: rolling_eyes :).

J'ai utilisé float ici, mais ʻint et double` font une différence significative, alors essayez-le. Cependant, en raison de la largeur des données, «double» n'est pas aussi différent que «int» et «float».

SLP est activé par défaut à partir de Java7u40, et si vous voulez le désactiver, utilisez l'option -XX: -UseSuperWord.

java8simd>java -XX:+UseSuperWord J8SIMD
vectorAdd: 4,624[msec]

java8simd>java -XX:-UseSuperWord J8SIMD
vectorAdd: 6,928[msec]

java8simd>java -version
java version "1.8.0_141"
Java(TM) SE Runtime Environment (build 1.8.0_141-b15)
Java HotSpot(TM) 64-Bit Server VM (build 25.141-b15, mixed mode)

Le temps d'exécution (millisecondes) de chaque opération est le suivant.

Calcul SLP activé SLP désactivé Rapport de vitesse
a[i] += b[i] 4,645 6,928 1.50
a[i] -= b[i] 4,647 7,029 1.52
a[i] *= b[i] 4,629 6,978 1.51
a[i] /= b[i] 4,357 12,151 2.79
a[i] %= b[i] - - -

J'ai abandonné parce que % = n'a pas fini après avoir attendu quelques minutes (pourquoi est-ce si lent?). L'addition, la soustraction et la multiplication ont montré une accélération de 1,5x. L'accélération de la division est particulièrement remarquable à près de 3 fois, mais y a-t-il un mouvement tel qu'un traitement sur un FPU dédié lorsqu'il est exécuté avec des instructions SIMD?

Ce qui suit est le résultat du passage à la version ʻint` et de l'exécution d'opérations sur les bits. Les opérations Bitshift ne semblent pas être optimisées dans SLP.

Calcul SLP activé SLP désactivé Rapport de vitesse
a[i] <<= b[i] 18,293 19,249 1.05
a[i] >>= b[i] 18,550 19,431 1.05
a[i] >>>= b[i] 19,862 20,105 1.01
a[i] |= b[i] 4,735 6,857 1.45
a[i] &= b[i] 4,756 6,799 1.43
a[i] ^= b[i] 4,697 7,532 1.60

Que SLP ait fonctionné efficacement ou non est un nombre remarquable, il semble donc que vous puissiez juger par la présence ou l'absence de l'option -XX: -UseSuperWord.

Conditions d'activation de SLP

Debug Compiled OpenJDK montrera comment les boucles sont perçues en utilisant l'option -XX: + PrintCompilation -XX: + TraceLoopOpts, et -XX: + PrintAssembly affichera l'assembly généré par HotSpot. est.

Java 8 / 9ea répertorié dans: rolling_eyes: Vectorisation dans HotSpot JVM Je vais mettre les conditions de. Je pense que les conditions de déclenchement et les restrictions seront assouplies dans les versions futures, veuillez donc suivre l'actualité.

for(int i=0; i<100; i++){ } // OK
for(int i=5; i<100; i++){ } // OK
for(int i=t0; i<t1; i++){ } // OK
for(int i=0; i<100; i+=4){ } // OK
for(int i=0; i<100; i+=d){ } // NG
for(int i=0; i<100; i*=2){ } // NG
for(int i=0; i<t1; i++){     // NG
  ...
  if(...) t1 ++;
}
for(byte i=0; i<100; i++){ }  // NG
for(char i=0; i<100; i++){ }  // OK
for(short i=0; i<100; i++){ } // OK
for(int i=0; i<100; i++){ }   // OK
for(long i=0; i<100; i++){ }  // NG
for(int i=0; i<100; i++){  // NG
  ...
  f();
}
for(int i=0; i<100; i++){  // OK
  a[i] += b[i];
}
for(int i=0; i<100; i++){  // NG
  a.set(i, a.get(i) + b.get(i));
}
a[i] ++;  // OK
a[i] += 2; // OK
a[i] *= b[i]; // OK
a[i] = b[i] + c[i]; // OK
for(int i=0; i<100; i++){  // NG
  a[i] += b[2 * i];
}
int r = 0;
for(int i=0; i<100; i++){
  r += a[i] * b[i];
}

Fonctionnalités comportementales et extensions Java 9

Sans surprise, les opérations API de collecte et Stream en Java (et les langages JavaVM tels que Scala et Kotlin) ne sont pas des opérations qui peuvent être traduites en instructions SIMD. Si le goulot d'étranglement est l'endroit où vous faites beaucoup de maths avec eux, il peut être intéressant d'envisager de le remplacer par un tableau et un nombre de boucles.

Puisque ʻUnsafe est un appel de méthode, vous ne pouvez pas vous attendre à cette optimisation dans le code qui utilise ʻUnsafe pour la prise de risque rapide.

La vectorisation a une contrainte d'alignement de 64 octets, et les fractions en avant du tableau sont effectuées dans une pré-boucle non vectorisée. De plus, les éléments qui ne sont pas des multiples de 4 à la fin du tableau sont post-bouclés sans être vectorisés. Cependant, la post-boucle semble être vectorisée dans Java 9.

Java 8 utilise des registres de 128 bits (xmm), il est donc vectorisé tous les 2 ou 4 éléments, mais en Java 9 AVX2 peut être augmenté à 32 ou quelque chose du genre.

SIMD

SIMD </ b> (Single Instruction Multiple Data) est un mécanisme pour appliquer la même instruction à un grand nombre de données numériques arrangées à la fois.

Le traitement arithmétique scientifique et technologique tel que la simulation de fluide, le traitement d'image, le traitement de la voix, le cryptage et le traitement d'apprentissage automatique impliquent souvent des opérations algébriques linéaires telles que "multiplier le tableau $ a $ par la valeur du tableau $ b $". Il sort en grande quantité.

L'unité de calcul vectoriel de GPU et de Spacon est une collection d'un grand nombre de cœurs de calcul peu coûteux, économes en énergie et peu encombrants qui éliminent les circuits inutiles pour effectuer une grande quantité de calculs. Ceux-ci peuvent appliquer une instruction à une grande quantité de données dans le registre à la fois.

Les processeurs récents ont des instructions de processeur qui effectuent plusieurs à des dizaines d'opérations vectorielles. Un GPU qui gère une grande quantité d'arithmétique dans le traitement d'image possède des milliers de processeurs dédiés à l'arithmétique et leur émet des instructions arithmétiques à la fois. Les deux sont SIMD, mais le SLP de HotSpot optimise les instructions du processeur.

Matériel de référence

Recommended Posts

Comment obtenir l'optimisation SIMD pour HotSpot JavaVM
Comment obtenir un heapdump à partir d'un conteneur Docker
Comment obtenir une classe depuis Element en Java
Comment obtenir JDK etc. depuis Oracle avec CLI
Comment obtenir les informations les plus longues de Twitter à partir du 12/12/2016
[IOS 14] Comment obtenir des données d'image de type Data directement à partir de PHPickerViewController?
Comment utiliser HttpClient de Java (Get)
Comment démarrer avec Slim
Comment obtenir des paramètres dans Spark
Comment passer du HTML à Haml
Comment obtenir et ajouter des données depuis Firebase Firestore dans Ruby
[Java] Comment convertir du type String en type Path et obtenir le chemin
[Kotlin] Trois façons d'obtenir un cours depuis KClass
[Rails] Comment convertir ERB en Haml
[Note] Comment démarrer avec Rspec
Comment appeler le code Swift 5.3 depuis Objective-C
[Java] Comment obtenir le répertoire actuel
[Flutter] Comment utiliser C / C ++ depuis Dart?
Java: Comment envoyer des valeurs du servlet au servlet
Comment obtenir la date avec Java
Comment obtenir la valeur du paramètre (valeur de la propriété) à partir de la base de données dans Spring Framework
Obtenez "2-4, 7, 9" de [4, 7, 9, 2, 3]
[Spring Boot] Comment obtenir des propriétés dynamiquement à partir d'une chaîne contenue dans une URL
Comment obtenir n'importe quel numéro de chiffre à partir de 2 chiffres ou plus! !!
Comment obtenir la date du type Date de JavaScript auquel les développeurs C # sont accros
Comment obtenir les informations d'identification Keycloak dans la classe d'intercepteur
Comment vider de la base de données (DB) vers le fichier de départ
Comment obtenir le jour d'aujourd'hui
Comment démarrer avec Eclipse Micro Profile
[Java] Comment obtenir des nombres aléatoires en excluant des nombres spécifiques
Comment obtenir et étudier Java SE8 Gold
Comment sortir un fichier de ressources avec spring-boot
[Java] Comment obtenir l'URL redirigée finale
[Rails] Comment émettre des messages de réussite et d'erreur
[ruby] Comment recevoir la valeur de l'entrée standard?
[Ruby] Récapitulatif de la façon d'obtenir des valeurs à partir d'une entrée standard [Mesures de contrôle des compétences Paiza]
[Swift] Comment lire des chansons de la bibliothèque musicale
Comment passer d'Eclipse Java à un fichier SQL
Comment écrire Scala du point de vue de Java
Comment déployer à partir d'une image Docker locale vers Heroku
Rails / Ruby: Comment obtenir du texte HTML pour Mail
[Java] Comment extraire le nom du fichier du chemin
Comment obtenir des informations sur les tables associées dans les tables plusieurs-à-plusieurs
Indiquez comment apprendre de Docker à AKS sur AWS
[Java] Comment obtenir la valeur maximale de HashMap
Comment supprimer des tuiles d'un projet vierge dans TERASOLUNA 5.x
Comment se connecter à ClearDB depuis Sequel Pro avec Heroku
[Java] Comment obtenir une requête par communication HTTP
Comment sortir le standard d'un tableau avec for Each
À partir d'avril 2018 Comment installer Java 8 sur Mac
Comment passer d'Oracle Java 8 à Adopt Open JDK 9
[Android] Comment obtenir la langue de réglage du terminal