Warum ist long langsamer als int in x64 Java?

Lesezeit: 14 Minuten

Benutzer-Avatar
Techrocket9

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 ein long 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 Vermutung ist, dass die CPU länger braucht, um eine 64-Bit-Ganzzahl hinzuzufügen als eine 32-Bit-Ganzzahl, 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 aus äquivalentem C++-Code, der von VS 2013 (unten) kompiliert wurde, dasselbe System. lang: 72265 ms int: 74656 ms Diese Ergebnisse waren im Debug-32-Bit-Modus.

Im 64-Bit-Release-Modus: lang: 875ms 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 eine CPU-Einschränkung 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: Habe dies gerade in Java 8 RTM erneut versucht, keine wesentliche Änderung.

  • Der wahrscheinlichste Verdächtige ist Ihre Einrichtung, nicht die CPU oder die verschiedenen Teile der JVM. Können Sie diese Messung zuverlässig reproduzieren? Die Schleife nicht wiederholen, den JIT nicht aufwärmen, verwenden currentTimeMillis()Laufcode, der trivial wegoptimiert werden kann, usw. stinkt nach unzuverlässigen Ergebnissen.

    Benutzer395760

    7. November 2013 um 18:44 Uhr


  • Ich war vor einiger Zeit Benchmarking, ich musste a verwenden long als Schleifenzähler, weil der JIT-Compiler die Schleife optimiert hat, wenn ich an verwendet habe int. Man müsste sich die Zerlegung des generierten Maschinencodes ansehen.

    – Sam

    7. November 2013 um 18:46 Uhr


  • Dies ist kein korrekter Mikrobenchmark, und ich würde nicht erwarten, dass seine Ergebnisse in irgendeiner Weise die Realität widerspiegeln.

    – Louis Wassermann

    7. November 2013 um 19:18 Uhr

  • Alle Kommentare, die das OP beschimpfen, weil es versäumt hat, einen richtigen Java-Mikrobenchmark zu schreiben, sind unsäglich faul. So etwas lässt sich sehr einfach herausfinden, wenn man sich anschaut, was die JVM mit dem Code macht.

    – tmyklebu

    7. November 2013 um 20:03 Uhr

  • @maaartinus: Akzeptierte Praxis ist akzeptierte Praxis, weil sie eine Liste bekannter Fallstricke umgeht. Im Fall von Proper Java Benchmarks möchten Sie sicherstellen, dass Sie richtig optimierten Code messen, keinen 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 hat dies angemessen demonstriert. Und wie bereits erwähnt, lässt die Umwandlung dieses Codes in einen richtigen Java-Benchmark die Verrücktheit nicht wirklich verschwinden. Und das Lesen von Assembler-Code ist nicht schwer.

    – tmyklebu

    7. November 2013 um 20:48 Uhr

Benutzer-Avatar
tmyklebu

Meine JVM führt diese ziemlich einfache Sache mit der inneren Schleife aus, wenn Sie sie verwenden 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 */

Es betrügt hart, wenn Sie es verwenden ints; Zuerst gibt es einige Verrücktheiten, die ich nicht zu verstehen behaupte, die aber wie eine Einrichtung für eine ausgerollte 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 ausgerollte 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 ausgerollte 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

Es geht also 16-mal schneller für Ints, weil die JIT die entrollt int Schleife 16 Mal, aber nicht entrollt long Schleife überhaupt.

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 ablegen, sonst schlägt sie fehl.

  • OK, das Netz ist also nicht das long Version ist langsamer, aber eher als die int Version ist schneller. Das macht Sinn. Wahrscheinlich wurde nicht so viel Aufwand in die JIT-Optimierung investiert long Ausdrücke.

    – Heiße Licks

    7. November 2013 um 21:45 Uhr

  • …verzeihen Sie meine Unwissenheit, aber was ist “funrolled”? Ich kann den Begriff anscheinend nicht einmal richtig googeln, und das ist das erste Mal, dass ich jemanden fragen muss, was ein Wort im Internet bedeutet.

    – BrianH

    8. November 2013 um 6:06 Uhr

  • @BrianDHall gcc Verwendet -f als Befehlszeilenschalter für “Flag” und die unroll-loops Die Optimierung wird eingeschaltet, indem man sagt -funroll-loops. Ich benutze einfach “unroll”, um die Optimierung zu beschreiben.

    – chrylis -vorsichtig optimistisch-

    8. November 2013 um 8:14 Uhr

  • @BRPocock: Der Java-Compiler kann das nicht, aber das JIT kann es sicher.

    – tmyklebu

    12. November 2013 um 19:48 Uhr

  • Nur um klar zu sein, es hat es nicht “lustig gemacht”. Es entrollte es UND konvertierte die entrollte Schleife in i-=16was natürlich 16x schneller ist.

    – Aleksandr Dubinsky

    23. November 2013 um 8:38 Uhr

Benutzer-Avatar
chrylis -vorsichtigoptimistisch-

Der JVM-Stack ist in Bezug auf definiert Wörter, dessen Größe ein Implementierungsdetail ist, aber mindestens 32 Bit breit sein muss. Der JVM-Implementierer kann Verwenden Sie 64-Bit-Wörter, aber der Bytecode kann sich nicht darauf verlassen, und daher Operationen mit long oder double Werte müssen mit besonderer Sorgfalt behandelt werden. Im Speziellen, die JVM-Integer-Verzweigungsanweisungen sind auf genau den Typ definiert int.

Im Fall Ihres Codes ist die Demontage aufschlussreich. Hier ist der Bytecode für die int Version wie von Oracle JDK 7 kompiliert:

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 Statik lädt i (0), subtrahieren Sie eins (3-4), duplizieren Sie den Wert auf dem Stapel (5) und schieben Sie ihn zurück in die Variable (6). Es führt dann eine Vergleiche-mit-Null-Verzweigung durch und kehrt zurück.

Die Version mit der long ist 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       

Erstens, wenn die JVM den neuen Wert auf dem Stack (5) dupliziert, muss sie zwei Stack-Wörter duplizieren. In Ihrem Fall ist es durchaus möglich, dass dies nicht teurer ist als das Duplizieren, da die JVM bei Bedarf ein 64-Bit-Wort verwenden kann. Sie werden jedoch feststellen, dass die Verzweigungslogik hier länger ist. Die JVM hat keine Anweisung zum Vergleichen von a long mit Null, also muss es eine Konstante drücken 0L auf den Stapel (9), mache einen General long Vergleich (10) und dann zum Wert von verzweigen das Berechnung.

Hier sind zwei plausible Szenarien:

  • Die JVM folgt genau dem Bytecode-Pfad. In diesem Fall leistet es mehr Arbeit in der long Version, Pushing und Popping mehrere zusätzliche Werte, und diese sind auf der virtueller verwalteter Stack, nicht der echte hardwareunterstützte CPU-Stack. 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 braucht es zusätzliche Zeit, um einen Teil der praktisch unnötigen Push/Compare-Logik wegzuoptimieren. Wenn dies der Fall ist, werden Sie nach dem Aufwärmen nur sehr geringe Leistungsunterschiede feststellen.

Ich empfehle Ihnen, einen korrekten Microbenchmark zu schreiben, um den Effekt des Einsetzens des JIT zu eliminieren, und dies auch mit einer Endbedingung zu versuchen, die nicht Null ist, um die JVM zu zwingen, denselben Vergleich auf der durchzuführen int das tut es mit dem long.

  • @Katona Nicht unbedingt. Ganz besonders, die Client- und Server-HotSpot-JVMs sind völlig unterschiedliche Implementierungen, und Ilya hat die Auswahl von Server nicht angegeben (Client ist normalerweise die 32-Bit-Standardeinstellung).

    – chrylis -vorsichtig optimistisch-

    7. November 2013 um 19:34 Uhr

  • @tmyklebu Das Problem ist, dass der Benchmark mehrere verschiedene Dinge gleichzeitig misst. Die Verwendung einer Endbedingung ungleich Null verringert die Anzahl der Variablen.

    – chrylis -vorsichtig optimistisch-

    7. November 2013 um 19:35 Uhr

  • @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) maßen sie nur Vergleiche und nur gegen 0, was ein Sonderfall ist. Nicht zuletzt 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 spezifischen Fall misst.

    – yshavit

    7. November 2013 um 20:17 Uhr

  • @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 anpasst, um den Unterschied, den es zu messen versucht, nicht mehr zu messen. OP kann mich korrigieren, wenn ich falsch liege, aber es sieht nicht so aus, als würden sie nur / hauptsächlich messen == 0, was einen überproportional großen Anteil an den Benchmark-Ergebnissen ausmacht. Es scheint mir wahrscheinlicher, dass OP versucht, einen allgemeineren Bereich von Operationen zu messen, und diese Antwort weist darauf hin, dass der Benchmark stark auf nur eine dieser Operationen ausgerichtet ist.

    – yshavit

    7. November 2013 um 20:21 Uhr


  • @tmyklebu Überhaupt nicht. Ich bin dafür, die Ursachen zu verstehen. Nachdem jedoch festgestellt wurde, dass eine der Hauptursachen darin besteht, dass der Benchmark verzerrt war, ist es nicht ungültig, den Benchmark zu ändern, um den Versatz zu entfernen. ebenso gut wie um mehr über diesen Skew zu erfahren (zum Beispiel, dass es einen effizienteren Bytecode ermöglichen kann, dass es das Entrollen von Schleifen erleichtern kann usw.). Aus diesem Grund habe ich sowohl diese Antwort (die die Verzerrung identifizierte) als auch Ihre (die sich eingehender mit der Verzerrung befasst) positiv bewertet.

    – yshavit

    7. November 2013 um 20:48 Uhr


Die grundlegende Dateneinheit in einer Java Virtual Machine ist ein Wort. Die Auswahl der richtigen Wortgröße bleibt der Implementierung der JVM überlassen. Eine JVM-Implementierung sollte eine Wortgröße von mindestens 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 schreibt nicht vor, dass auch die Wortgröße gleich sein sollte. JVM liest/schreibt Daten Wort für Wort. Aus diesem Grund kann es für a länger dauern lang als ein int.

Hier Sie können mehr zum gleichen Thema finden.

Benutzer-Avatar
Tucuxi

Ich habe gerade einen Benchmark mit geschrieben Bremssattel.

Das Ergebnisse sind ziemlich konsistent mit dem ursprünglichen Code: eine ~12-fache Beschleunigung für die Verwendung int Über long. Es scheint sicherlich, dass das von tmyklebu gemeldete Abrollen der Schleife oder etwas sehr Ähnliches vor sich geht.

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 caliperda ich nicht herausfinden konnte, wie ich gegen ihre vorhandene Beta-Version codieren sollte.

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

Fürs Protokoll, diese Version macht ein grobes “Warmup”:

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 den beiden bleibt ungefähr gleich.

  • @TedHopp – Ich habe versucht, die Loop-Limits in meinem zu ändern, und es blieb im Wesentlichen unverändert.

    – Heiße Licks

    7. November 2013 um 20:00 Uhr

  • @Techrocket9: Ich bekomme ähnliche Zahlen (int ist 20mal schneller) mit diesem Code.

    – tmyklebu

    7. November 2013 um 20:44 Uhr

Benutzer-Avatar
R.Möller

Für die Aufzeichnungen:

wenn ich benutze

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

(geändert “l–” zu “l = l – 1l”) lange Leistung verbessert sich um ~50%

  • @TedHopp – Ich habe versucht, die Loop-Limits in meinem zu ändern, und es blieb im Wesentlichen unverändert.

    – Heiße Licks

    7. November 2013 um 20:00 Uhr

  • @Techrocket9: Ich bekomme ähnliche Zahlen (int ist 20mal schneller) mit diesem Code.

    – tmyklebu

    7. November 2013 um 20:44 Uhr

Benutzer-Avatar
Vlad L

Dies liegt wahrscheinlich daran, dass die JVM bei Verwendung von long (ungezählte Schleife) nach Sicherungspunkten sucht und dies nicht für int (gezählte Schleife) tut.

Einige Referenzen: https://stackoverflow.com/a/62557768/14624235

https://stackoverflow.com/a/58726530/14624235

http://psy-lob-saw.blogspot.com/2016/02/wait-for-it-counteduncounted-loops.html

1270370cookie-checkWarum ist long langsamer als int in x64 Java?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy