94 votes

Pourquoi est bien plus lente que les int x64 Java ?

Je suis sous Windows 8.1 64 bits avec Java 7 update 45 x 64 (n ° 32 bits de Java installée sur une Surface Pro 2 comprimé.

Le code ci-dessous prend 1688ms lorsque le type de i est une longue et 109ms quand je est un int. Pourquoi est long (64 bits type) d'un ordre de grandeur inférieure à celle de l'int sur une plateforme 64 bits avec une JVM 64 bits?

Ma seule la spéculation est que le CPU prend plus de temps pour ajouter un 64 bits en entier de 32 bits, mais cela semble peu probable. Je soupçonne Haswell ne pas utiliser ripple carry additionneurs.

Je suis de l'exécution de cette dans Eclipse Kepler SR1, btw.

public class Main {

    private static long i = Integer.MAX_VALUE;

    public static void main(String[] args) {    
        System.out.println("Starting the loop");
        long startTime = System.currentTimeMillis();
        while(!decrementAndCheck()){
        }
        long endTime = System.currentTimeMillis();
        System.out.println("Finished the loop in " + (endTime - startTime) + "ms");
    }

    private static boolean decrementAndCheck() {
        return --i < 0;
    }

}

Edit: Voici les résultats de l'équivalent de code C++ compilé par VS 2013 (ci-dessous), même système. long: 72265ms int: 74656ms Ces résultats ont été en debug 32 bits mode.

En 64 bits mode de diffusion: long: 875ms long long: 906ms int: 1047ms

Ceci suggère que le résultat que j'ai observé est de la JVM d'optimisation de la folie plutôt que de CPU limites.

#include "stdafx.h"
#include "iostream"
#include "windows.h"
#include "limits.h"

long long i = INT_MAX;

using namespace std;


boolean decrementAndCheck() {
return --i < 0;
}


int _tmain(int argc, _TCHAR* argv[])
{


cout << "Starting the loop" << endl;

unsigned long startTime = GetTickCount64();
while (!decrementAndCheck()){
}
unsigned long endTime = GetTickCount64();

cout << "Finished the loop in " << (endTime - startTime) << "ms" << endl;



}

Edit: Juste essayé une fois de plus à Java 8 RTM, aucun changement significatif.

83voto

tmyklebu Points 7075

Mon JVM ne ce assez simple chose à l'intérieur de la boucle lorsque vous utilisez longs:

0x00007fdd859dbb80: test   %eax,0x5f7847a(%rip)  /* fun JVM hack */
0x00007fdd859dbb86: dec    %r11                  /* i-- */
0x00007fdd859dbb89: mov    %r11,0x258(%r10)      /* store i to memory */
0x00007fdd859dbb90: test   %r11,%r11             /* unnecessary test */
0x00007fdd859dbb93: jge    0x00007fdd859dbb80    /* go back to the loop top */

Il triche, difficile, lorsque vous utilisez ints; d'abord, il y a quelques screwiness que je ne prétends pas comprendre, mais ressemble à l'installation d'une funrolled boucle:

0x00007f3dc290b5a1: mov    %r11d,%r9d
0x00007f3dc290b5a4: dec    %r9d
0x00007f3dc290b5a7: mov    %r9d,0x258(%r10)
0x00007f3dc290b5ae: test   %r9d,%r9d
0x00007f3dc290b5b1: jl     0x00007f3dc290b662
0x00007f3dc290b5b7: add    $0xfffffffffffffffe,%r11d
0x00007f3dc290b5bb: mov    %r9d,%ecx
0x00007f3dc290b5be: dec    %ecx              
0x00007f3dc290b5c0: mov    %ecx,0x258(%r10)   
0x00007f3dc290b5c7: cmp    %r11d,%ecx
0x00007f3dc290b5ca: jle    0x00007f3dc290b5d1
0x00007f3dc290b5cc: mov    %ecx,%r9d
0x00007f3dc290b5cf: jmp    0x00007f3dc290b5bb
0x00007f3dc290b5d1: and    $0xfffffffffffffffe,%r9d
0x00007f3dc290b5d5: mov    %r9d,%r8d
0x00007f3dc290b5d8: neg    %r8d
0x00007f3dc290b5db: sar    $0x1f,%r8d
0x00007f3dc290b5df: shr    $0x1f,%r8d
0x00007f3dc290b5e3: sub    %r9d,%r8d
0x00007f3dc290b5e6: sar    %r8d
0x00007f3dc290b5e9: neg    %r8d
0x00007f3dc290b5ec: and    $0xfffffffffffffffe,%r8d
0x00007f3dc290b5f0: shl    %r8d
0x00007f3dc290b5f3: mov    %r8d,%r11d
0x00007f3dc290b5f6: neg    %r11d
0x00007f3dc290b5f9: sar    $0x1f,%r11d
0x00007f3dc290b5fd: shr    $0x1e,%r11d
0x00007f3dc290b601: sub    %r8d,%r11d
0x00007f3dc290b604: sar    $0x2,%r11d
0x00007f3dc290b608: neg    %r11d
0x00007f3dc290b60b: and    $0xfffffffffffffffe,%r11d
0x00007f3dc290b60f: shl    $0x2,%r11d
0x00007f3dc290b613: mov    %r11d,%r9d
0x00007f3dc290b616: neg    %r9d
0x00007f3dc290b619: sar    $0x1f,%r9d
0x00007f3dc290b61d: shr    $0x1d,%r9d
0x00007f3dc290b621: sub    %r11d,%r9d
0x00007f3dc290b624: sar    $0x3,%r9d
0x00007f3dc290b628: neg    %r9d
0x00007f3dc290b62b: and    $0xfffffffffffffffe,%r9d
0x00007f3dc290b62f: shl    $0x3,%r9d
0x00007f3dc290b633: mov    %ecx,%r11d
0x00007f3dc290b636: sub    %r9d,%r11d
0x00007f3dc290b639: cmp    %r11d,%ecx
0x00007f3dc290b63c: jle    0x00007f3dc290b64f
0x00007f3dc290b63e: xchg   %ax,%ax /* OK, fine; I know what a nop looks like */

puis le funrolled boucle elle-même:

0x00007f3dc290b640: add    $0xfffffffffffffff0,%ecx
0x00007f3dc290b643: mov    %ecx,0x258(%r10)
0x00007f3dc290b64a: cmp    %r11d,%ecx
0x00007f3dc290b64d: jg     0x00007f3dc290b640

puis le démontage du code de la funrolled boucle, lui-même un test et une ligne droite de la boucle:

0x00007f3dc290b64f: cmp    $0xffffffffffffffff,%ecx
0x00007f3dc290b652: jle    0x00007f3dc290b662
0x00007f3dc290b654: dec    %ecx
0x00007f3dc290b656: mov    %ecx,0x258(%r10)
0x00007f3dc290b65d: cmp    $0xffffffffffffffff,%ecx
0x00007f3dc290b660: jg     0x00007f3dc290b654

Il en va de 16 fois plus rapide pour ints parce que le JIT funrolled l' int boucle de 16 fois, mais n'a pas funroll l' long boucle à tous.

Pour être complet, voici le code que j'ai essayé:

public class foo136 {
  private static int i = Integer.MAX_VALUE;
  public static void main(String[] args) {
    System.out.println("Starting the loop");
    for (int foo = 0; foo < 100; foo++)
      doit();
  }

  static void doit() {
    i = Integer.MAX_VALUE;
    long startTime = System.currentTimeMillis();
    while(!decrementAndCheck()){
    }
    long endTime = System.currentTimeMillis();
    System.out.println("Finished the loop in " + (endTime - startTime) + "ms");
  }

  private static boolean decrementAndCheck() {
    return --i < 0;
  }
}

L'assemblée décharges ont été générés en utilisant les options -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly. Notez que vous avez besoin de perdre son temps avec votre installation JVM pour avoir ce travail pour vous; vous avez besoin de mettre un peu aléatoire bibliothèque partagée dans exactement le bon endroit ou il échouera.

22voto

chrylis Points 22655

La JVM de la pile est définie en termes de mots, dont la taille est un détail d'implémentation, mais doit être d'au moins 32 bits de large. Responsable de l'implémentation de la JVM peut utiliser 64 bits des mots, mais le bytecode peut pas compter sur ce, et pour les opérations avec long ou double valeurs doivent être manipulés avec soin. En particulier, la JVM entier les instructions de branchement sont définis sur exactement le type int.

Dans le cas de votre code, le démontage est très instructive. Voici le pseudo-code pour l' int version compilées par l'Oracle JDK 7:

private static boolean decrementAndCheck();
  Code:
     0: getstatic     #14  // Field i:I
     3: iconst_1      
     4: isub          
     5: dup           
     6: putstatic     #14  // Field i:I
     9: ifge          16
    12: iconst_1      
    13: goto          17
    16: iconst_0      
    17: ireturn       

Notez que la JVM va charger la valeur de votre statique i (0), soustrayez un (3-4), le double de la valeur sur la pile (5), et le pousser à nouveau dans la variable (6). Il fait ensuite une comparaison avec le zéro de la branche et les retours.

La version avec l' long est un peu plus compliqué:

private static boolean decrementAndCheck();
  Code:
     0: getstatic     #14  // Field i:J
     3: lconst_1      
     4: lsub          
     5: dup2          
     6: putstatic     #14  // Field i:J
     9: lconst_0      
    10: lcmp          
    11: ifge          18
    14: iconst_1      
    15: goto          19
    18: iconst_0      
    19: ireturn       

Tout d'abord, lorsque la JVM doublons la nouvelle valeur sur la pile (5), il a pour dupliquer deux pile de mots. Dans votre cas, il est tout à fait possible que ce n'est pas plus cher que de dupliquer un, depuis le JVM est libre d'utiliser une version 64 bits du mot si commode. Cependant, vous remarquerez que la direction de la logique est plus ici. La JVM n'a pas d'instruction pour comparer un long avec zéro, de sorte qu'il a du pousser une constante 0L sur la pile (9), faire un long de la comparaison (10), et puis la branche sur la valeur de que calcul.

Voici deux scénarios plausibles:

  • La JVM est à la suite de la bytecode chemin exactement. Dans ce cas, c'est de faire plus de travail dans l' long version, de pousser et de popping plusieurs valeurs supplémentaires, et ceux-ci sont sur le virtuel géré pile, pas la vraie assistance matérielle CPU de la pile. Si c'est le cas, vous aurez toujours de voir une différence de performances significative après l'échauffement.
  • La JVM se rend compte qu'il peut optimiser ce code. Dans ce cas, c'est prendre plus de temps pour optimiser la pratiquement inutile push/comparer logique. Si c'est le cas, vous verrez que très peu de différence de performance après l'échauffement.

Je vous recommande d' écrire un bon microbenchmark pour éliminer l'effet d'avoir le JIT coup de pied dans, et aussi d'essayer cela avec une dernière condition qui n'est pas zéro, à force de la JVM de faire la même comparaison sur l' int que c'est le cas avec l' long.

8voto

Vaibhav Raj Points 935

L'unité de base de données dans une Machine Virtuelle Java est la parole. Choisir la bonne taille de mot est à gauche sur la mise en œuvre de la JVM. Une JVM doit choisir une taille minimale des mots de 32 bits. Il peut augmenter la taille de mot pour un gain d'efficacité. Ni il n'y a aucune restriction qu'une JVM 64 bits devrait choisir 64 bits seule parole.

L'architecture sous-jacente n'a pas de règles que la taille de mot doit être la même. JVM lit/écrit des données par mot. C'est la raison pourquoi il peut être prendre plus de temps pour une longue qu'un int.

Ici vous pouvez trouver plus sur le même sujet.

4voto

tucuxi Points 5130

J'ai juste écrit un repère à l'aide de l'étrier.

Les résultats sont tout à fait cohérents avec le code d'origine: a ~12x speedup pour l'utilisation de int sur long. Il semble bien que le déroulement de la boucle rapporté par tmyklebu ou quelque chose de très similaire est en cours.

timeIntDecrements         195,266,845.000
timeLongDecrements      2,321,447,978.000

C'est mon code; note qu'il utilise un fraîchement construit instantané d' caliper, puisque je ne pouvais pas comprendre comment le code par rapport à leur version bêta.

package test;

import com.google.caliper.Benchmark;
import com.google.caliper.Param;

public final class App {

    @Param({""+1}) int number;

    private static class IntTest {
        public static int v;
        public static void reset() {
            v = Integer.MAX_VALUE;
        }
        public static boolean decrementAndCheck() {
            return --v < 0;
        }
    }

    private static class LongTest {
        public static long v;
        public static void reset() {
            v = Integer.MAX_VALUE;
        }
        public static boolean decrementAndCheck() {
            return --v < 0;
        }
    }

    @Benchmark
    int timeLongDecrements(int reps) {
        int k=0;
        for (int i=0; i<reps; i++) {
            LongTest.reset();
            while (!LongTest.decrementAndCheck()) { k++; }
        }
        return (int)LongTest.v | k;
    }    

    @Benchmark
    int timeIntDecrements(int reps) {
        int k=0;
        for (int i=0; i<reps; i++) {
            IntTest.reset();
            while (!IntTest.decrementAndCheck()) { k++; }
        }
        return IntTest.v | k;
    }
}

1voto

Hot Licks Points 25075

Pour mémoire, cette version fait un brut « warm up » :

Le Global times améliorer environ 30 %, mais le rapport entre les deux reste à peu près le même.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X