web-dev-qa-db-fra.com

Pourquoi est-il plus lent que int en Java x64?

J'utilise Windows 8.1 x64 avec Java 7 mise à jour 45 x64 (pas de 32 bits Java installé) sur une tablette Surface Pro 2).

Le code ci-dessous prend 1688 ms lorsque le type de i est long et 109 ms lorsque i est un int. Pourquoi est long (un type 64 bits) un ordre de grandeur plus lent que int sur une plate-forme 64 bits avec une machine virtuelle Java 64 bits?

Ma seule spéculation est que le processeur prend plus de temps pour ajouter un entier 64 bits qu'un entier 32 bits, mais cela semble peu probable. Je soupçonne que Haswell n'utilise pas d'additionneurs à ondulation.

J'exécute cela 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 du code C++ équivalent compilé par VS 2013 (ci-dessous), même système. long: 72265ms int: 74656ms Ces résultats étaient en mode débogage 32 bits.

En mode de libération 64 bits: long: 875ms long long: 906ms int: 1047ms

Cela suggère que le résultat que j'ai observé est la bizarrerie de l'optimisation JVM plutôt que les limitations du processeur.

#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: Je viens de réessayer dans Java 8 RTM, aucun changement significatif.

90
Techrocket9

Ma machine virtuelle Java fait cette chose assez simple pour la boucle interne 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 dur quand vous utilisez ints; il y a d'abord un peu de vis que je ne prétends pas comprendre mais qui ressemble à la configuration d'une boucle déroulée:

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 la boucle déroulée elle-même:

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

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

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

Cela va donc 16 fois plus vite pour les entiers car le JIT a déroulé la boucle int 16 fois, mais n'a pas déroulé la boucle long du tout.

Pour être complet, voici le code que j'ai réellement 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;
  }
}

Les vidages d'assemblage ont été générés à l'aide des options -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly. Notez que vous devez jouer avec votre installation JVM pour que cela fonctionne également pour vous; vous devez mettre une bibliothèque partagée aléatoire exactement au bon endroit ou elle échouera.

79
tmyklebu

La pile JVM est définie en termes de mots , dont la taille est un détail d'implémentation mais doit avoir une largeur d'au moins 32 bits. L'implémenteur JVM peut utiliser des mots 64 bits, mais le bytecode ne peut pas compter sur cela, et donc les opérations avec long ou double les valeurs doivent être manipulées avec un soin supplémentaire. En particulier, les instructions de la branche entière JVM sont définies exactement sur le type int.

Dans le cas de votre code, le démontage est instructif. Voici le bytecode pour la version int tel que compilé par 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 chargera la valeur de votre _ i (0) statique, en soustraira un (3-4), dupliquera la valeur sur la pile (5) et la repoussera dans la variable (6) . Il effectue ensuite une branche de comparaison avec zéro et retourne.

La version avec le long est un peu plus compliquée:

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       

Premièrement, lorsque la JVM duplique la nouvelle valeur sur la pile (5), elle doit dupliquer deux mots de pile. Dans votre cas, il est tout à fait possible que cela ne soit pas plus cher que la duplication, car la JVM est libre d'utiliser un mot 64 bits si cela vous convient. Cependant, vous remarquerez que la logique de branche est plus longue ici. La JVM n'a pas d'instructions pour comparer un long avec zéro, elle doit donc pousser une constante 0L sur la pile (9), faites une comparaison générale de long (10), puis branchez-vous sur la valeur de qui calcul.

Voici deux scénarios plausibles:

  • La JVM suit exactement le chemin du bytecode. Dans ce cas, il fait plus de travail dans la version long, en poussant et en sautant plusieurs valeurs supplémentaires, et celles-ci sont sur la pile gérée virtuelle , pas la vraie pile CPU assistée par matériel. Si tel est le cas, vous constaterez toujours une différence de performances significative après l'échauffement.
  • La JVM se rend compte qu'elle peut optimiser ce code. Dans ce cas, il faut plus de temps pour optimiser une partie de la logique Push/Compare pratiquement inutile. Si tel est le cas, vous constaterez très peu de différence de performances après l'échauffement.

Je vous recommande écrivez une bonne référence pour éliminer l'effet d'avoir le coup d'envoi JIT, et aussi essayer ceci avec une condition finale qui n'est pas nulle, pour forcer la JVM à faire la même comparaison sur le int qu'il fait avec le long.

22
chrylis

Unité de base des données dans une Java est Word. Le choix de la bonne taille de mot est laissé lors de l'implémentation de la JVM. Une implémentation de la JVM doit choisir une taille minimale de mot de 32 bits. Elle peut choisissez une taille de mot plus élevée pour gagner en efficacité. Il n'y a pas non plus de restriction qu'une JVM 64 bits ne choisisse que le mot 64 bits.

L'architecture sous-jacente ne stipule pas que la taille de Word doit également être la même. JVM lit/écrit des données mot par mot. C’est la raison pour laquelle cela pourrait prendre plus de temps longue qu'un int.

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

8
Vaibhav Raj

Je viens d'écrire un benchmark en utilisant caliper .

Les résultats sont assez cohérents avec le code d'origine: une accélération de ~ 12x pour l'utilisation de int sur long. Il semble certainement que la boucle se déroule rapportée par tmykleb ou quelque chose de très similaire se passe.

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

Ceci est mon code; notez qu'il utilise un instantané fraîchement construit de caliper, car je n'ai pas pu comprendre comment coder par rapport à leur version bêta existante.

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;
    }
}
4
tucuxi

Pour les dossiers:

si j'utilise

boolean decrementAndCheckLong() {
    lo = lo - 1l;
    return lo < -1l;
}

(changé "l--" en "l = l - 1l") les performances longues s'améliorent de ~ 50%

1
R.Moeller

Pour mémoire, cette version fait un "warmup" grossier:

public class LongSpeed {

    private static long i = Integer.MAX_VALUE;
    private static int j = Integer.MAX_VALUE;

    public static void main(String[] args) {

        for (int x = 0; x < 10; x++) {
            runLong();
            runWord();
        }
    }

    private static void runLong() {
        System.out.println("Starting the long loop");
        i = Integer.MAX_VALUE;
        long startTime = System.currentTimeMillis();
        while(!decrementAndCheckI()){

        }
        long endTime = System.currentTimeMillis();

        System.out.println("Finished the long loop in " + (endTime - startTime) + "ms");
    }

    private static void runWord() {
        System.out.println("Starting the Word loop");
        j = Integer.MAX_VALUE;
        long startTime = System.currentTimeMillis();
        while(!decrementAndCheckJ()){

        }
        long endTime = System.currentTimeMillis();

        System.out.println("Finished the Word loop in " + (endTime - startTime) + "ms");
    }

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

    private static boolean decrementAndCheckJ() {
        return --j < 0;
    }

}

Le temps global s'améliore d'environ 30%, mais le rapport entre les deux reste à peu près le même.

1
Hot Licks

Je n'ai pas de machine 64 bits pour tester, mais la différence assez importante suggère qu'il y a plus que le bytecode légèrement plus long au travail.

Je vois des temps très proches pour long/int (4400 vs 4800ms) sur mon 32 bits 1.7.0_45.

Ce n'est qu'un deviner, mais je fortement soupçonne que c'est l'effet d'une pénalité de désalignement de la mémoire. Pour confirmer/infirmer le soupçon, essayez d'ajouter un mannequin statique public int = 0; avant la déclaration de i. Cela poussera i vers le bas de 4 octets dans la disposition de la mémoire et peut le rendre correctement aligné pour de meilleures performances. Confirmé comme ne provoquant pas le problème.

ÉDITER: Le raisonnement derrière cela est que le VM ne peut pas réorganiser champs à loisir en ajoutant un rembourrage pour un alignement optimal, car cela peut interférer avec JNI (Pas le cas).

0
Durandal