Warum ist in x64 Java lange langsamer als int?

90

Ich verwende Windows 8.1 x64 mit Java 7 Update 45 x64 (kein 32-Bit-Java installiert) auf einem Surface Pro 2-Tablet.

Der folgende Code benötigt 1688 ms, wenn der Typ von i lang ist, und 109 ms, wenn i ein int ist. Warum ist long (ein 64-Bit-Typ) auf einer 64-Bit-Plattform mit einer 64-Bit-JVM um eine Größenordnung langsamer als int?

Meine einzige Spekulation ist, dass die CPU länger braucht, um eine 64-Bit-Ganzzahl als eine 32-Bit-Ganzzahl hinzuzufügen, aber das scheint unwahrscheinlich. Ich vermute, Haswell verwendet keine Ripple-Carry-Addierer.

Ich führe dies übrigens in Eclipse Kepler SR1 aus.

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

}

Bearbeiten: Hier sind die Ergebnisse von äquivalentem C ++ - Code, der von VS 2013 (unten), demselben System, kompiliert wurde. lang: 72265 ms int: 74656 ms Diese Ergebnisse befanden sich im 32-Bit-Debug-Modus.

Im 64-Bit-Freigabemodus: lang: 875 ms lang lang: 906 ms int: 1047 ms

Dies deutet darauf hin, dass das Ergebnis, das ich beobachtet habe, eher eine Verrücktheit der JVM-Optimierung als CPU-Einschränkungen ist.

#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;



}

Bearbeiten: Ich habe es gerade noch einmal in Java 8 RTM versucht, keine wesentliche Änderung.

Techrocket9
quelle
8
Der wahrscheinlichste Verdächtige ist Ihre Einrichtung, nicht die CPU oder die verschiedenen Teile der JVM. Können Sie diese Messung zuverlässig reproduzieren? Das Nicht-Wiederholen der Schleife, das Nicht-Aufwärmen der JIT, das currentTimeMillis()Ausführen, Ausführen von Code, der trivial vollständig optimiert werden kann usw. stinkt nach unzuverlässigen Ergebnissen.
1
Ich habe vor einiger Zeit ein Benchmarking durchgeführt und musste a longals Schleifenzähler verwenden, da der JIT-Compiler die Schleife optimiert hat, als ich eine verwendet habe int. Man müsste sich die Demontage des generierten Maschinencodes ansehen.
Sam
7
Dies ist kein korrektes Mikrobenchmark, und ich würde nicht erwarten, dass seine Ergebnisse die Realität in irgendeiner Weise widerspiegeln.
Louis Wasserman
7
Alle Kommentare, die das OP beschimpfen, weil es kein richtiges Java-Mikrobenchmark geschrieben hat, sind unbeschreiblich faul. Dies ist sehr einfach herauszufinden, wenn Sie nur schauen und sehen, was die JVM mit dem Code macht.
tmyklebu
2
@maaartinus: Akzeptierte Praxis ist akzeptierte Praxis, da sie eine Liste bekannter Fallstricke umgeht. Im Fall von Proper Java Benchmarks möchten Sie sicherstellen, dass Sie ordnungsgemäß optimierten Code messen, nicht einen On-Stack-Ersatz, und Sie möchten sicherstellen, dass Ihre Messungen am Ende sauber sind. OP fand ein völlig anderes Problem, und der von ihm bereitgestellte Benchmark zeigte es angemessen. Und wie bereits erwähnt, lässt das Verwandeln dieses Codes in einen richtigen Java-Benchmark die Verrücktheit nicht wirklich verschwinden. Und das Lesen von Assembler-Code ist nicht schwer.
tmyklebu

Antworten:

80

Meine JVM macht diese ziemlich einfache Sache mit der inneren Schleife, wenn Sie longs verwenden:

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 */

Es betrügt schwer, wenn Sie ints verwenden; Zuerst gibt es einige Irrtümer, die ich nicht zu verstehen behaupte, die aber wie ein Setup für eine abgewickelte Schleife aussehen:

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 */

dann die abgewickelte Schleife selbst:

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

dann der Teardown-Code für die abgewickelte Schleife, selbst ein Test und eine gerade Schleife:

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

Für Ints geht es also 16-mal schneller, weil die JIT die intSchleife 16-mal abgewickelt hat, die Schleife aber überhaupt nicht abgewickelt hat long.

Der Vollständigkeit halber hier der Code, den ich tatsächlich ausprobiert habe:

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

Die Assembly-Dumps wurden mit den Optionen generiert -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly. Beachten Sie, dass Sie mit Ihrer JVM-Installation herumspielen müssen, damit dies auch für Sie funktioniert. Sie müssen eine zufällige gemeinsam genutzte Bibliothek genau an der richtigen Stelle platzieren, da dies sonst fehlschlägt.

tmyklebu
quelle
8
OK, das net-net ist also nicht, dass die longVersion langsamer ist, sondern dass die Version intschneller ist. Das macht Sinn. Wahrscheinlich wurde nicht so viel Aufwand in die Optimierung der longAusdrücke durch die JIT investiert .
Hot Licks
1
... verzeihen Sie meine Unwissenheit, aber was ist "funrolled"? Ich kann den Begriff nicht einmal richtig googeln, und deshalb musste ich zum ersten Mal jemanden fragen, was ein Wort im Internet bedeutet.
BrianH
1
@BrianDHall gccverwendet -fals Befehlszeilenschalter für "flag", und die unroll-loopsOptimierung wird aktiviert , indem gesagt wird -funroll-loops. Ich benutze nur "Abrollen", um die Optimierung zu beschreiben.
Chrylis
4
@BRPocock: Der Java-Compiler kann nicht, aber die JIT kann es sicher.
tmyklebu
1
Nur um klar zu sein, es hat es nicht "funroll". Es hat es entrollt UND die entrollte Schleife in konvertiert i-=16, was natürlich 16x schneller ist.
Aleksandr Dubinsky
22

Der JVM-Stapel wird in Form von Wörtern definiert , deren Größe ein Implementierungsdetail ist, die jedoch mindestens 32 Bit breit sein muss. Der JVM-Implementierer verwendet möglicherweise 64-Bit-Wörter, aber der Bytecode kann sich nicht darauf verlassen. Daher müssen Operationen mit longoder doubleWerte mit besonderer Sorgfalt behandelt werden. Insbesondere sind die JVM-Integer-Verzweigungsbefehle genau für den Typ definiert int.

Bei Ihrem Code ist die Demontage aufschlussreich. Hier ist der Bytecode für die intvom Oracle JDK 7 kompilierte Version:

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       

Beachten Sie, dass die JVM den Wert Ihrer statischen Aufladung lädt i (0) , einen (3-4) subtrahiert, den Wert auf dem Stapel (5) dupliziert und ihn zurück in die Variable (6) schiebt. Es führt dann einen Vergleich mit Null durch und gibt zurück.

Die Version mit dem longist etwas komplizierter:

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       

Wenn die JVM den neuen Wert auf dem Stapel (5) dupliziert, muss sie zunächst zwei Stapelwörter duplizieren. In Ihrem Fall ist es durchaus möglich, dass dies nicht teurer ist als das Duplizieren eines Wortes, da die JVM bei Bedarf ein 64-Bit-Wort verwenden kann. Sie werden jedoch feststellen, dass die Verzweigungslogik hier länger ist. Die JVM keinen Befehl haben zu einem Vergleich longmit Null, so dass es einem konstanten drücken hat 0Lauf den Stapel (9), habe einen allgemeinen longVergleich (10), und dann Zweig auf dem Wert , dass Berechnung .

Hier sind zwei plausible Szenarien:

  • Die JVM folgt genau dem Bytecode-Pfad. In diesem Fall wird in der longVersion mehr Arbeit geleistet , indem mehrere zusätzliche Werte verschoben und gepoppt werden. Diese befinden sich auf dem virtuell verwalteten Stapel und nicht auf dem realen hardwareunterstützten CPU-Stapel. Wenn dies der Fall ist, werden Sie nach dem Aufwärmen immer noch einen signifikanten Leistungsunterschied feststellen.
  • Die JVM erkennt, dass sie diesen Code optimieren kann. In diesem Fall dauert es länger, einige der praktisch unnötigen Push / Compare-Logik zu optimieren. Wenn dies der Fall ist, werden Sie nach dem Aufwärmen nur einen sehr geringen Leistungsunterschied feststellen.

Ich empfehle Sie , einen korrekten - Micro schreiben die Wirkung des mit dem JIT - Kick in, zu beseitigen und dies auch mit einem Endzustand versuchen , die nicht Null ist, um die JVM zu zwingen , denselben Vergleich zu folgendem Thema zu tun , intdass es mit der tut long.

chrylis -vorsichtig optimistisch-
quelle
1
@ Katona Nicht unbedingt. Insbesondere sind die Client- und Server-HotSpot-JVMs völlig unterschiedliche Implementierungen, und Ilya hat nicht angegeben, dass Server ausgewählt werden soll (Client ist normalerweise die 32-Bit-Standardeinstellung).
Chrylis
1
@tmyklebu Das Problem ist, dass der Benchmark mehrere verschiedene Dinge gleichzeitig misst. Die Verwendung einer Terminalbedingung ungleich Null reduziert die Anzahl der Variablen.
Chrylis
1
@tmyklebu Der Punkt ist, dass das OP beabsichtigt hatte, die Geschwindigkeit von Inkrementen, Dekrementen und Vergleichen von Ints und Longs zu vergleichen. Stattdessen (vorausgesetzt, diese Antwort ist richtig) haben sie nur Vergleiche gemessen und nur gegen 0, was ein Sonderfall ist. Wenn nichts anderes, macht es den ursprünglichen Benchmark irreführend - es sieht so aus, als würde es drei allgemeine Fälle messen, obwohl es tatsächlich einen bestimmten Fall misst.
Yshavit
1
@tmyklebu Versteh mich nicht falsch, ich habe die Frage, diese Antwort und deine Antwort positiv bewertet. Aber ich stimme Ihrer Aussage nicht zu, dass @chrylis den Benchmark so anpasst, dass die Messung des Unterschieds, den es zu messen versucht, nicht mehr gemessen wird. OP kann mich korrigieren, wenn ich falsch liege, aber es sieht nicht so aus, als würden sie nur / hauptsächlich messen == 0, was ein unverhältnismäßig großer Teil der Benchmark-Ergebnisse zu sein scheint. Es scheint mir wahrscheinlicher, dass OP versucht, einen allgemeineren Bereich von Operationen zu messen, und diese Antwort weist darauf hin, dass die Benchmark stark auf nur eine dieser Operationen ausgerichtet ist.
Yshavit
2
@ tmyklebu Überhaupt nicht. Ich bin alle dafür, die Ursachen zu verstehen. Nachdem festgestellt wurde, dass eine der Hauptursachen darin besteht, dass der Benchmark verzerrt wurde, ist es nicht ungültig, den Benchmark zu ändern, um den Versatz zu entfernen, sowie diesen Versatz zu untersuchen und besser zu verstehen (zum Beispiel, dass er effizienter sein kann) Bytecode, der das Abrollen von Schleifen usw. erleichtern kann. Aus diesem Grund habe ich sowohl diese Antwort (die den Versatz identifiziert hat) als auch Ihre (die sich eingehender mit dem Versatz befasst) positiv bewertet.
Yshavit
8

Die grundlegende Dateneinheit in einer Java Virtual Machine ist das Wort. Die Auswahl der richtigen Wortgröße bleibt bei der Implementierung der JVM. Eine JVM-Implementierung sollte eine Mindestwortgröße von 32 Bit wählen. Es kann eine höhere Wortgröße wählen, um die Effizienz zu steigern. Es gibt auch keine Einschränkung, dass eine 64-Bit-JVM nur 64-Bit-Wörter auswählen sollte.

Die zugrunde liegende Architektur regelt nicht, dass die Wortgröße auch gleich sein sollte. JVM liest / schreibt Daten Wort für Wort. Dies ist der Grund , warum es länger ein vielleicht nimmt lange als ein int .

Hier finden Sie weitere Informationen zum gleichen Thema.

Vaibhav Raj
quelle
4

Ich habe gerade einen Benchmark mit Bremssattel geschrieben .

Die Ergebnisse stimmen ziemlich gut mit dem ursprünglichen Code überein: eine ~ 12-fache Beschleunigung für die Verwendung von intover long. Es scheint sicher, dass die von tmyklebu oder etwas sehr Ähnlichem gemeldete Schleifenabwicklung stattfindet .

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

Das ist mein Code; Beachten Sie, dass es einen frisch erstellten Snapshot von verwendet caliper, da ich nicht herausfinden konnte, wie man gegen die vorhandene Beta-Version codiert.

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
quelle
1

Für die Aufzeichnung macht diese Version ein grobes "Aufwärmen":

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

}

Die Gesamtzeiten verbessern sich um etwa 30%, aber das Verhältnis zwischen beiden bleibt ungefähr gleich.

Hot Licks
quelle
@ TedHopp - Ich habe versucht, die Loop-Limits in meinem zu ändern, und es blieb im Wesentlichen unverändert.
Hot Licks
@ Techrocket9: Ich bekomme ähnliche Zahlen ( intist 20-mal schneller) mit diesem Code.
tmyklebu
1

Für die Aufzeichnungen:

wenn ich benutze

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

(geändert "l--" in "l = l - 1l") Die Langzeitleistung verbessert sich um ~ 50%

R. Moeller
quelle
0

Ich habe keine 64-Bit-Maschine zum Testen, aber der ziemlich große Unterschied deutet darauf hin, dass mehr als der etwas längere Bytecode am Werk ist.

Ich sehe sehr nahe Zeiten für long / int (4400 vs 4800ms) auf meinem 32-Bit 1.7.0_45.

Dies ist nur eine Vermutung , aber ich vermute stark , dass dies die Auswirkung einer Strafe für Speicherfehlausrichtung ist. Versuchen Sie, einen öffentlichen statischen int-Dummy = 0 hinzuzufügen, um den Verdacht zu bestätigen / abzulehnen. Vor der Erklärung von i. Dadurch wird i im Speicherlayout um 4 Byte nach unten gedrückt und möglicherweise für eine bessere Leistung richtig ausgerichtet. Es wurde bestätigt, dass das Problem nicht verursacht wird.

BEARBEITEN: Der Grund dafür ist, dass die VM Felder möglicherweise nicht neu anordnet anordnet und für eine optimale Ausrichtung eine Auffüllung hinzufügt, da dies die JNI beeinträchtigen kann (Nicht der Fall).

Durandal
quelle
Die VM sicher ist zu Neuordnungs Feldern und Add Polsterung erlaubt.
Hot Licks
JNI muss über diese lästigen, langsamen Zugriffsmethoden auf Objekte zugreifen, die ohnehin einige undurchsichtige Handles benötigen, da GC auftreten kann, während nativer Code ausgeführt wird. Es ist reichlich kostenlos, Felder neu zu ordnen und Polster hinzuzufügen.
tmyklebu