Pourquoi long est-il plus lent que int dans Java x64?

90

J'utilise Windows 8.1 x64 avec la mise à jour de Java 7 45 x64 (aucun Java 32 bits 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 long (un type 64 bits) est-il un ordre de grandeur plus lent que int sur une plate-forme 64 bits avec une JVM 64 bits?

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

J'exécute ceci 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: longue: 875ms long long: 906ms int: 1047ms

Cela suggère que le résultat que j'ai observé est l'étrangeté de l'optimisation de la 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, pas de changement significatif.

Techrocket9
la source
8
Le suspect le plus probable est votre configuration, pas le CPU ou les différentes parties de la JVM. Pouvez-vous reproduire cette mesure de manière fiable? Ne pas répéter la boucle, ne pas réchauffer le JIT, utiliser currentTimeMillis(), exécuter du code qui peut être complètement optimisé de manière triviale, etc. sentent les résultats peu fiables.
1
Je faisais un benchmarking il y a quelque temps, je devais utiliser a longcomme compteur de boucle, car le compilateur JIT optimisait la sortie de boucle, quand j'utilisais un int. Il faudrait regarder le désassemblage du code machine généré.
Sam
7
Ce n'est pas un microbenchmark correct, et je ne m'attendrais pas à ce que ses résultats reflètent la réalité de quelque manière que ce soit.
Louis Wasserman
7
Tous les commentaires reprochant à l'OP de ne pas avoir écrit un microbenchmark Java approprié sont incroyablement paresseux. C'est le genre de chose qui est très facile à comprendre si vous regardez et voyez ce que la JVM fait au code.
tmyklebu
2
@maaartinus: Une pratique acceptée est une pratique acceptée car elle fonctionne autour d'une liste de pièges connus. Dans le cas de Benchmarks Java appropriés, vous voulez vous assurer que vous mesurez du code correctement optimisé, pas un remplacement sur pile, et vous voulez vous assurer que vos mesures sont propres à la fin. OP a trouvé un problème complètement différent et le point de repère qu'il a fourni le démontrait adéquatement. Et, comme indiqué, transformer ce code en un Benchmark Java approprié ne fait pas disparaître l'étrangeté. Et la lecture du code d'assemblage n'est pas difficile.
tmyklebu

Réponses:

80

Ma JVM fait cette chose assez simple à 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; d'abord, il y a des vilains que je ne prétends pas comprendre mais qui ressemble à une configuration pour 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, elle-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 intboucle 16 fois, mais n'a pas du longtout déroulé la boucle .

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 manipuler votre installation JVM pour que cela fonctionne également pour vous; vous devez placer une bibliothèque partagée aléatoire exactement au bon endroit ou elle échouera.

tmyklebu
la source
8
OK, donc le net-net n'est pas que la longversion est plus lente, mais plutôt que la intversion est plus rapide. Ça a du sens. Probablement pas autant d'efforts ont été investis pour que le JIT optimise les longexpressions.
Hot Licks
1
... pardonnez mon ignorance, mais qu'est-ce que "funrolled"? Je n'arrive même pas à rechercher correctement le terme sur Google, et c'est la première fois que je dois demander à quelqu'un ce qu'un mot signifie sur Internet.
BrianH
1
@BrianDHall gccutilise -fcomme commutateur de ligne de commande pour "drapeau", et l' unroll-loopsoptimisation est activée en disant -funroll-loops. J'utilise simplement "dérouler" pour décrire l'optimisation.
chrylis -cautfullyoptimistic-
4
@BRPocock: Le compilateur Java ne peut pas, mais le JIT le peut.
tmyklebu
1
Juste pour être clair, ça ne "funroll" pas. Il l'a déroulé ET converti la boucle déroulée en i-=16, ce qui est bien sûr 16 fois plus rapide.
Aleksandr Dubinsky
22

La pile JVM 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. L'implémenteur JVM peut utiliser des mots de 64 bits, mais le bytecode ne peut pas s'appuyer sur cela, et les opérations avec longou doublevaleurs doivent donc être traitées avec un soin particulier. En particulier, les instructions de 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 de la intversion compilée 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 statique i(0), 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 renvoie.

La version avec le longest 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 fort possible que ce ne soit pas plus cher que d'en dupliquer un, car la JVM est libre d'utiliser un mot de 64 bits si cela vous convient. Cependant, vous remarquerez que la logique de branche est plus longue ici. La JVM n'a pas d'instruction pour comparer a longavec zéro, elle doit donc pousser une constante 0Lsur la pile (9), faire une longcomparaison générale (10), puis bifurquer sur la valeur de ce calcul.

Voici deux scénarios plausibles:

  • La JVM suit exactement le chemin du bytecode. Dans ce cas, il fait plus de travail dans la longversion, en poussant et en affichant plusieurs valeurs supplémentaires, et celles-ci se trouvent sur la pile gérée virtuelle , pas sur la vraie pile de CPU assistée par matériel. Si tel est le cas, vous constaterez toujours une différence de performances significative après le préchauffage.
  • 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 verrez très peu de différence de performances après l'échauffement.

Je vous recommande d' écrire un microbenchmark correct pour éliminer l'effet du démarrage du JIT, et d'essayer également cela avec une condition finale qui n'est pas nulle, pour forcer la JVM à faire la même comparaison intavec le long.

chrylis - prudemment optimiste -
la source
1
@Katona Pas nécessairement. En particulier, les JVM HotSpot client et serveur sont des implémentations complètement différentes, et Ilya n'a pas indiqué de sélectionner Serveur (le client est généralement la valeur par défaut 32 bits).
chrylis
1
@tmyklebu Le problème est que le benchmark mesure plusieurs choses différentes à la fois. L'utilisation d'une condition terminale différente de zéro réduit le nombre de variables.
chrylis -cautfullyoptimistic-
1
@tmyklebu Le fait est que l'OP avait l'intention de comparer la vitesse des incréments, des décrémentations et des comparaisons entre les entiers et les longs. Au lieu de cela (en supposant que cette réponse est correcte), ils ne mesuraient que des comparaisons, et uniquement par rapport à 0, ce qui est un cas particulier. Si rien d'autre, cela rend le point de repère d'origine trompeur - il semble qu'il mesure trois cas généraux, alors qu'en fait, il mesure un cas spécifique.
yshavit
1
@tmyklebu Ne vous méprenez pas, j'ai voté pour la question, cette réponse et votre réponse. Mais je ne suis pas d'accord avec votre affirmation selon laquelle @chrylis ajuste l'indice de référence pour arrêter de mesurer la différence qu'il essaie de mesurer. OP peut me corriger si je me trompe, mais il ne semble pas qu'ils essaient de mesurer uniquement / principalement == 0, ce qui semble être une partie disproportionnée des résultats de référence. Il me semble plus probable qu'OP essaie de mesurer une gamme plus générale d'opérations, et cette réponse souligne que le point de référence est fortement biaisé vers une seule de ces opérations.
yshavit
2
@tmyklebu Pas du tout. Je suis tout à fait d'accord pour comprendre les causes profondes. Mais, après avoir identifié cette cause fondamentale majeure est que le benchmark était biaisé, il n'est pas invalide de modifier le benchmark pour supprimer le biais, ainsi que de creuser et de mieux comprendre ce biais (par exemple, qu'il peut permettre une bytecode, qu'il peut faciliter le déroulement des boucles, etc.). C'est pourquoi j'ai voté à la fois cette réponse (qui a identifié le biais) et la vôtre (qui creuse plus en détail le biais).
yshavit
8

L'unité de base des données dans une machine virtuelle Java est le mot. Le choix de la bonne taille de mot est laissé lors de la mise en œuvre de la JVM. Une implémentation JVM doit choisir une taille de mot minimale de 32 bits. Il peut choisir une taille de mot plus élevée pour gagner en efficacité. Il n'y a pas non plus de restriction selon laquelle une JVM 64 bits doit choisir uniquement un mot 64 bits.

L'architecture sous-jacente ne règle pas que la taille des mots doit également être la même. JVM lit / écrit les données mot par mot. C'est la raison pour laquelle cela peut prendre plus de temps qu'un int .

Ici vous pouvez en savoir plus sur le même sujet.

Vaibhav Raj
la source
4

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

Les résultats sont tout à fait cohérents avec le code d'origine: une accélération de ~ 12x pour l'utilisation de intover long. Il semble certainement que le déroulement de la boucle rapporté par tmyklebu ou quelque chose de très similaire se déroule.

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

Ceci est mon code; notez qu'il utilise un instantané nouvellement construit de caliper, car je ne pouvais pas 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;
    }
}
tucuxi
la source
1

Pour mémoire, cette version fait un "échauffement" 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;
    }

}

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

Hot Licks
la source
@TedHopp - J'ai essayé de changer les limites de boucle dans la mienne et cela est resté essentiellement inchangé.
Hot Licks du
@ Techrocket9: j'obtiens des nombres similaires ( intc'est 20 fois plus rapide) avec ce code.
tmyklebu
1

Pour les archives:

si j'utilise

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

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

R.Moeller
la source
0

Je n'ai pas de machine 64 bits pour tester, mais la différence plutôt grande 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 1.7.0_45 32 bits.

Ce n'est qu'une supposition , mais je soupçonne fortement que c'est l'effet d'une pénalité de désalignement de la mémoire. Pour confirmer / nier la suspicion, essayez d'ajouter un public static int dummy = 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 causant pas le problème.

ÉDITER: Le raisonnement derrière cela est que la machine virtuelle ne peut pas réorganiser les champs à sa guise en ajoutant un remplissage pour un alignement optimal, car cela peut interférer avec JNI (Pas le cas).

Durandal
la source
La VM est certainement autorisée à réorganiser les champs et à ajouter un remplissage.
Hot Licks
JNI doit accéder aux objets via ces méthodes d'accès ennuyeuses et lentes qui prennent de toute façon quelques poignées opaques puisque GC peut se produire pendant l'exécution du code natif. Il est très gratuit de réorganiser les champs et d'ajouter du remplissage.
tmyklebu